Skip to content

feat(workflow-executor): allow custom DB schema via DATABASE_SCHEMA env var#1754

Open
nbouliol wants to merge 4 commits into
mainfrom
feature/prd-773-workflow-executor-allow-custom-postgres-schema-via
Open

feat(workflow-executor): allow custom DB schema via DATABASE_SCHEMA env var#1754
nbouliol wants to merge 4 commits into
mainfrom
feature/prd-773-workflow-executor-allow-custom-postgres-schema-via

Conversation

@nbouliol

@nbouliol nbouliol commented Jul 13, 2026

Copy link
Copy Markdown
Member

Summary

The standalone workflow-executor CLI was locked to the Postgres forest schema. It read no schema env var and never set database.schema, so resolveSchema always fell back to DEFAULT_SCHEMA. Library consumers could already pass database.schema — this closes the CLI gap.

Changes

  • cli-core.ts: new DATABASE_SCHEMA env var (mirrors DATABASE_SSL) — read into CliConfig, injected into the database options, surfaced in --help and the startup log. Blank/unset resolves to the forest default.
  • Tests covering env read (value / unset / blank), forwarding to buildDatabase, and the startup-log schema.
  • README: documented DATABASE_SCHEMA in the Database connection table + note.

No store/build changes: the downstream plumbing and forest fallback already existed.

Test plan

  • yarn workspace @forestadmin/workflow-executor test -- test/cli.test.ts (108 passed)
  • yarn workspace @forestadmin/workflow-executor lint (0 errors)
  • yarn workspace @forestadmin/workflow-executor build

Fixes PRD-773

🤖 Generated with Claude Code

Note

Add DATABASE_SCHEMA env var support to workflow-executor for custom Postgres schema

  • Adds a DATABASE_SCHEMA environment variable to cli-core.ts that sets the Postgres schema used by the executor, defaulting to "forest".
  • Validates the value as a valid Postgres identifier (max 63 chars, enforced regex); blank/whitespace values are treated as unset and invalid values throw a ConfigurationError on startup.
  • Forwards the resolved schema to DatabaseExecutorOptions when set, and includes it in structured startup logs.
  • Risk: the executor requires CREATE privilege on the database at boot time due to CREATE SCHEMA IF NOT EXISTS.

Changes since #1754 opened

  • Modified runMigrations in schema-migrations.ts to probe pg_namespace for schema existence before attempting schema creation [b6c7707]
  • Added tests in database-store-schema.test.ts to verify schema existence probing behavior [b6c7707]
  • Updated documentation in CLAUDE.md and README.md to document schema existence probing and reduced privilege requirements [b6c7707]

Macroscope summarized f5efb65.

…nv var

The standalone CLI was locked to the `forest` schema: it read no schema
env var and never set `database.schema`, so resolveSchema always fell
back to DEFAULT_SCHEMA. Add a DATABASE_SCHEMA env var (mirroring
DATABASE_SSL) so the CLI can target another schema; blank/unset keeps
the `forest` default. Library consumers could already pass
database.schema — this closes the CLI gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 13, 2026

Copy link
Copy Markdown

PRD-773

…up log

Replace the bare 'forest' literal in logStartup with the existing
DEFAULT_SCHEMA constant, removing the duplicated default, and drop the
redundant what/where comment on the DATABASE_SCHEMA read.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@qltysh

qltysh Bot commented Jul 13, 2026

Copy link
Copy Markdown

Qlty


Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (2)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/stores/schema-migrations.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/cli-core.ts100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

@matthv

matthv commented Jul 13, 2026

Copy link
Copy Markdown
Member

Nice — clean change and the DEFAULT_SCHEMA refactor addresses the duplicated-default nit. Two points still worth considering before merge:

1. Validate DATABASE_SCHEMA (fail-fast)

env.DATABASE_SCHEMA?.trim() flows unescaped all the way into raw DDL in schema-migrations.ts:

sequelize.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`, { transaction });
// and quoted references: `"${schema}"."${tableName}"`

Before this PR the value was always the forest constant, so there was no surface. Now it's operator-controlled — a malformed value (typo, embedded ", etc.) fails at boot with a cryptic SQL error instead of a clear message. Suggest validating at the boundary in readEnvConfig, reusing the existing ConfigurationError pattern:

const schema = env.DATABASE_SCHEMA?.trim();
if (schema && !/^[a-zA-Z_][a-zA-Z0-9_$]*$/.test(schema)) {
  throw new ConfigurationError('DATABASE_SCHEMA must be a valid PostgreSQL identifier');
}

(The regex follows PG's unquoted-identifier rules; can be relaxed to only reject "/NUL if exotic names ever need supporting.)

2. Document the CREATE privilege requirement (README)

On boot the executor runs CREATE SCHEMA IF NOT EXISTS, which requires the CREATE privilege on the database when the schema doesn't exist — and in Postgres the ACL check happens before the IF NOT EXISTS short-circuit, so pre-creating the schema doesn't remove that requirement. This is exactly the constraint behind PRD-773, so a one-liner near the DATABASE_SCHEMA note would help:

The executor creates the schema on boot if it doesn't exist, which requires the CREATE privilege on the database. To avoid granting it, pre-create the schema and grant USAGE, CREATE on it to the executor's DB user.

Neither is functionally blocking; (1) is the hardening I'd recommend since this PR is what makes the schema configurable.

… privilege

The schema now flows into raw DDL, so validate it as a Postgres
identifier at the boundary (fail fast with ConfigurationError instead
of a cryptic SQL error), and reject values past the 63-char limit
Postgres would silently truncate. Document in the README that boot runs
CREATE SCHEMA IF NOT EXISTS, which needs CREATE on the database even
when the schema already exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nbouliol

nbouliol commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Both addressed in f5efb65.

1. ValidationDATABASE_SCHEMA is now validated at the boundary in readEnvConfig via a parseSchemaEnv helper that throws ConfigurationError for non-identifiers (used your regex ^[a-zA-Z_][a-zA-Z0-9_$]*$), plus a 63-char cap since Postgres silently truncates longer identifiers and would target a different schema. Covered by tests (rejects leading digit, dash, space, embedded ", dot, and >63 chars).

2. README — documented. I verified the privilege behavior empirically on PG 15 rather than trust my assumption: with a role lacking CREATE on the database, CREATE SCHEMA IF NOT EXISTS analytics fails with permission denied for database even when analytics already exists. So you are right — the ACL check fires before the IF NOT EXISTS short-circuit, and pre-creating does not remove the requirement. I reworded the note accordingly (GRANT CREATE ON DATABASE), and dropped the misleading "pre-create to avoid it" workaround.

Worth noting this requirement is not new to this PR: the boot-time CREATE SCHEMA IF NOT EXISTS already ran for the default forest schema, so the executor has always needed CREATE on the database. This PR only makes the schema name configurable.

@matthv matthv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

…xists

Postgres checks the database-level CREATE privilege for CREATE SCHEMA IF
NOT EXISTS even when the schema is present, so an operator who pre-creates
the schema still needed database-level CREATE just to boot. Probe
pg_namespace first (world-readable, no privilege) inside the migration
lock and skip the CREATE when the schema exists, so a DB user with only
schema-level CREATE on a pre-created schema can run the executor. Document
both grant paths in the README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants