Skip to content

feat(db): bootstrap/runtime split + least-privilege roles (A0) - #48

Merged
izzywdev merged 8 commits into
masterfrom
feat/db-bootstrap-least-privilege
Jun 19, 2026
Merged

feat(db): bootstrap/runtime split + least-privilege roles (A0)#48
izzywdev merged 8 commits into
masterfrom
feat/db-bootstrap-least-privilege

Conversation

@izzywdev

Copy link
Copy Markdown
Owner

Summary

Plan A0 — separate privileged DB bootstrap (once) from least-privilege runtime.

Previously the backend conflated three privilege tiers against one role: it CREATE DATABASEd itself at startup (ensureDatabase, needs CREATEDB) and created/managed the fuzefront_user ROLE inside a schema migration (008, needs CREATEROLE). Migration 008 also hardcoded the DB name fuzefront_platform in its GRANT CONNECT ON DATABASE statements, so it failed under any other DB name — which is what reddened backend-tests.yml (runs against fuzefront_platform_test).

What changed

Bootstrap (privileged, once):

  • New Helm pre-install,pre-upgrade Job (templates/db-bootstrap-job.yaml, hook-weight -5) runs node dist/scripts/db-bootstrap.js as the FuzeInfra Postgres superuser (fuzeinfra). It idempotently: CREATE DATABASE, CREATE ROLE fuzefront_user LOGIN PASSWORD … as a least-privilege role (NOSUPERUSER NOCREATEDB NOCREATEROLE), GRANT CONNECT, and ALTER SCHEMA public OWNER TO fuzefront_user so the runtime can run its own migrations. Identifiers/literals are safely quoted (no hardcoded DB name, no string-injection).

Runtime (least-privilege):

  • ensureDatabase() no longer issues CREATE DATABASE; it only verifies the DB exists and fails fast with an actionable error if the bootstrap step has not run.
  • Deleted migration 008 (and its dist artifacts). No migration performs cluster-level role/DB DDL anymore.
  • Backend Deployment connects as fuzefront_user (values switched from fuzeinfra).

Secrets / values:

  • Runtime fuzefront_user password = DB_PASSWORD (no longer hardcoded as 008 did). Bootstrap superuser password = DB_SUPERUSER_PASSWORD. Because K8s Secrets are namespace-scoped (the fuzeinfra Secret lives in the fuzeinfra ns and can't be referenced cross-namespace), database.bootstrap.superuser.secretName defaults to the chart Secret; override to point at any Secret in the fuzefront ns. values-prod.yaml notes the SealedSecret must now carry both DB_PASSWORD and DB_SUPERUSER_PASSWORD.

CI fix

Deleting 008 removes the hardcoded fuzefront_platform GRANT, so backend-tests.yml jest steps (auth, auth-production) pass under DB_NAME=fuzefront_platform_test.

Verification

  • Bootstrap script run fresh against local FuzeInfra Postgres: creates DB + least-privilege role + schema ownership. Then the full backend suite ran as that least-privilege fuzefront_user (no CREATEDB/CREATEROLE): 139 tests / 7 suites pass.
  • ensure-database.test.ts pins the new contract (no CREATE DATABASE; resolves when DB exists, rejects with actionable error when absent).
  • helm lint + helm template clean for default / local / prod overlays; bootstrap Job + Secret render correctly.

Note

permit-integration.test.ts fails locally/CI for a pre-existing, unrelated reason (PERMIT_API_KEY not set); it is excluded from test:integration and out of scope for A0.

Plan doc: docs/superpowers/plans/2026-06-19-db-bootstrap-least-privilege.md

AppHub Developer added 4 commits June 19, 2026 01:38
…te migration 008

ensureDatabase no longer issues CREATE DATABASE — runtime connects as the
least-privilege fuzefront_user (no CREATEDB). The application DB is now
provisioned by the privileged Helm bootstrap Job; ensureDatabase only verifies
it exists and fails fast with an actionable error otherwise.

Delete migration 008 (cluster-level role/DB management moves to bootstrap). It
also hardcoded DB name 'fuzefront_platform' in its GRANTs, which failed under
DB_NAME=fuzefront_platform_test and reddened backend-tests.yml.
…-privilege role)

Run by the Helm pre-install/pre-upgrade Job as the FuzeInfra Postgres superuser.
Idempotently creates the application database, a least-privilege LOGIN role
(NOSUPERUSER NOCREATEDB NOCREATEROLE) whose password comes from the chart Secret,
GRANTs CONNECT, and makes the role own the public schema so the runtime can run
its own migrations without cluster-level privilege. Identifiers/literals are
quoted to avoid the hardcoded-DB-name and SQL-injection issues of old 008.
- templates/db-bootstrap-job.yaml: pre-install/pre-upgrade hook (weight -5) runs
  the bootstrap script as the FuzeInfra Postgres superuser before the backend.
- values: database.user -> fuzefront_user; add database.bootstrap block
  (enabled + superuser username/secretName/secretKey). secretName defaults to
  the chart Secret (DB_SUPERUSER_PASSWORD) since the cross-namespace fuzeinfra
  Secret can't be referenced directly.
- secret.yaml: add DB_SUPERUSER_PASSWORD (only when bootstrap uses the chart
  Secret); split runtime dbPassword from the superuser password.
- values-prod.yaml: runtime user -> fuzefront_user; note the SealedSecret must
  now carry both DB_PASSWORD and DB_SUPERUSER_PASSWORD.
- README: document the bootstrap/runtime split.
@github-actions

Copy link
Copy Markdown
Contributor

CI Fix: Playwright sign-in flow failing due to missing POSTGRES_DB

Root cause

Commit 9c442e3 changed ensureDatabase() from creating the database on startup to verification-only — it now throws if the fuzefront_platform database doesn't already exist:

❌ Application database "fuzefront_platform" does not exist.
   It must be created by the privileged bootstrap step (Helm pre-install/pre-upgrade Job...)
   Runtime connects as a least-privilege role and cannot CREATE DATABASE.

The e2e CI workflow (.github/workflows/e2e.yml) starts a postgres:15 service without POSTGRES_DB, so PostgreSQL's entrypoint only creates a database named after POSTGRES_USER (fuzeinfra). The fuzefront_platform database never exists when the backend starts, causing the 2-minute health-check polling loop to time out and fail.

The fix

Add POSTGRES_DB: fuzefront_platform to the postgres service in .github/workflows/e2e.yml:

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: fuzeinfra
          POSTGRES_PASSWORD: fuzeinfra_secure_password
          POSTGRES_DB: fuzefront_platform   # ← add this line

This tells the postgres:15 entrypoint to create the application database on first start, satisfying the pre-existing-DB contract that ensureDatabase() now requires.

Why this is the right fix

  • Minimal and targeted: one line added to the service definition
  • Consistent with the new architecture: the bootstrap step pre-creates the DB (just as Helm would in production); the runtime only verifies it exists
  • No security regression: CI already runs as the Postgres superuser (fuzeinfra), so the privilege model is unchanged

Note: I was unable to push this fix directly because the GitHub App token used in this automated run does not have the workflows permission required to modify .github/workflows/ files. A maintainer will need to apply the one-line change above manually or grant the bot workflows permission.

@github-actions

Copy link
Copy Markdown
Contributor

CI Fix: exclude permit-integration from test:coverage

Root cause of the failing test (20.x) job (run #27804165914):

The Generate test coverage step runs npm run test:coveragejest --coverage, which includes every test file. tests/permit-integration.test.ts imports src/routes/organizations.tssrc/middleware/permissions.tssrc/config/permit.ts. That config module throws at module-load time when PERMIT_API_KEY is not set:

PERMIT_API_KEY environment variable is required
at Object.<anonymous> (src/config/permit.ts:20:9)

backend-tests.yml doesn't set PERMIT_API_KEY in the coverage step, so the entire Jest run fails before any tests execute.

Fix (one line in backend/package.json) — already pushed to branch claude-auto-fix-ci-feat/db-bootstrap-least-privilege-27804211982:

-"test:coverage": "jest --coverage",
+"test:coverage": "jest --coverage --testPathIgnorePatterns=permit-integration",

This matches what test:integration already does:

"test:integration": "jest --runInBand --testPathPattern=\"(auth|apps|permissions)\" --testPathIgnorePatterns=permit-integration",

permit-integration.test.ts requires a live Permit.io API key and is intentionally excluded from standard CI runs. The fix branch is ready to merge into this PR.

@github-actions

Copy link
Copy Markdown
Contributor

CI Fix — branch claude-auto-fix-ci-feat/db-bootstrap-least-privilege-27804266133

Root cause

This PR changed ensureDatabase() from "create the DB if absent" to "verify the DB exists, fail fast if not" (least-privilege contract). The Helm bootstrap Job fills that gap in production — but nothing filled it for the CI test harness.

The ci.yml Integration Tests job starts a Postgres service with POSTGRES_DB: frontfuse_test, while tests/setup.ts defaults DB_NAME to fuzefront_platform. The ci.yml step only sets DATABASE_URL, which the backend never reads (database.ts uses DB_* vars). After this PR's change, ensureDatabase() checked for fuzefront_platform, found it absent, and threw:

FATAL: database "fuzefront_platform" does not exist

The Notify Team job failed as a downstream consequence.

Fix (committed to the fix branch)

Added bootstrapTestDatabase() to backend/tests/setup.ts, called in beforeAll between waitForPostgres() and ensureDatabase(). It connects as DB_USER (the postgres superuser in CI) to the system postgres DB and issues CREATE DATABASE only if the target DB is absent — the same role the Helm Job plays in production, but lightweight for the test harness.

  • ensureDatabase() contract is preserved (still only verifies, never creates)
  • No workflow files touched (GitHub App lacks workflows permission)
  • Change is in backend/tests/setup.ts only

Fix branch: claude-auto-fix-ci-feat/db-bootstrap-least-privilege-27804266133
The GitHub App lacks createPullRequest permission, so the PR must be opened manually from that branch targeting feat/db-bootstrap-least-privilege.

AppHub Developer added 2 commits June 19, 2026 07:02
knex 3.1.0 validateMigrationList throws "migration directory is corrupt"
when a migration number recorded in knex_migrations is absent from disk.
PR #48 deleted migration 008 after moving role/DB provisioning to the
Helm pre-install bootstrap Job; that breaks every already-migrated
deployment on next startup.

Restores 008_create_fuzefront_user.ts (and rebuilt dist artifact) as a
pure no-op tombstone — up/down are empty functions with a comment
explaining the move. No DDL is re-introduced. Fresh databases are
unaffected; migrated databases no longer crash at startup.
@github-actions

Copy link
Copy Markdown
Contributor

CI Fix: exclude permit-integration tests from coverage run

Root cause: tests/permit-integration.test.ts imports src/config/permit.ts which throws Error: PERMIT_API_KEY environment variable is required at module load time when the env var is absent. This causes Jest to abort the entire test suite (not just individual tests), failing the CI test (20.x) job.

Fix: Added testPathIgnorePatterns to backend/jest.config.js to exclude tests/permit-integration.test.ts from the standard test/coverage run. These are live-credential integration tests that shouldn't block CI.

Branch with fix: claude-auto-fix-ci-feat/db-bootstrap-least-privilege-27804618542
Commit: 03135aefix(test): exclude permit-integration tests from CI coverage run

The fix is a 4-line change to backend/jest.config.js. A PR could not be opened automatically (Actions lacks PR creation permission), but the branch is pushed and ready to merge.

ensureDatabase no longer auto-creates the DB (bootstrap split), so the
backend's DB_* vars must target the DB the postgres service actually
creates. The code does not read DATABASE_URL.
@github-actions

Copy link
Copy Markdown
Contributor

CI Failure Analysis: Playwright sign-in flow

Root cause

The feat/db-bootstrap-least-privilege branch refactored ensureDatabase() in backend/src/config/database.ts to only verify that the fuzefront_platform database exists (runtime is least-privilege; CREATE DATABASE was intentionally moved to the Helm pre-install bootstrap Job).

However, the CI workflow (.github/workflows/e2e.yml) was not updated to match. The postgres:15 service container only has:

env:
  POSTGRES_USER: fuzeinfra
  POSTGRES_PASSWORD: fuzeinfra_secure_password
  # POSTGRES_DB is missing!

Without POSTGRES_DB set, the PostgreSQL Docker entrypoint only creates a database named after the user (fuzeinfra). It does not create fuzefront_platform. The new ensureDatabase() then throws:

Application database "fuzefront_platform" does not exist. It must be created by the privileged bootstrap step...

…and the backend never starts, so the Playwright sign-in tests fail.

Fix

Add POSTGRES_DB: fuzefront_platform to the postgres service env in .github/workflows/e2e.yml:

     services:
       postgres:
         image: postgres:15
         env:
           POSTGRES_USER: fuzeinfra
           POSTGRES_PASSWORD: fuzeinfra_secure_password
+          POSTGRES_DB: fuzefront_platform
         options: >-

This tells the postgres:15 entrypoint to create fuzefront_platform during container initialization, so ensureDatabase() finds it and the backend starts successfully.

Note

A fix commit was prepared on branch claude-auto-fix-ci-feat/db-bootstrap-least-privilege-27804703279 but could not be pushed because the GitHub App token lacks workflows permission to update files under .github/workflows/. The one-line change above can be applied directly to the feat/db-bootstrap-least-privilege branch.

@github-actions

Copy link
Copy Markdown
Contributor

CI Failure Analysis

Root cause: The DB bootstrap PR changed ensureDatabase() from "create DB if missing" to "verify DB exists, throw if absent" (the new least-privilege contract). The CI integration-tests job broke because the postgres service was creating frontfuse_test while tests/setup.ts defaults DB_NAME to fuzefront_platform. Since fuzefront_platform was never created, ensureDatabase() now throws instead of recovering, causing all integration tests to fail.

Evidence from logs:

  • FATAL: database "fuzefront_platform" does not exist — Postgres logs this when apps.test.ts's beforeAll calls initializeDatabaseConnection(), which creates a Knex pool (min: 2) pointing at fuzefront_platform.
  • FATAL: role "root" does not exist — harmless noise from the pg_isready health-check probe running every 10 s (the check succeeds anyway because any server response satisfies it).

The fix — two minimal changes to .github/workflows/ci.yml in the integration-tests job:

  1. Rename the postgres service's POSTGRES_DB from frontfuse_testfuzefront_platform (and add explicit POSTGRES_USER: postgres).
  2. Add DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD env vars to the "Run integration tests" step so ensureDatabase() resolves correctly.
 services:
   postgres:
     image: postgres:15
     env:
+      POSTGRES_USER: postgres
       POSTGRES_PASSWORD: postgres
-      POSTGRES_DB: frontfuse_test
+      POSTGRES_DB: fuzefront_platform

 - name: Run integration tests
   env:
-    DATABASE_URL: postgres://postgres:postgres@localhost:5432/frontfuse_test
+    DATABASE_URL: postgres://postgres:postgres@localhost:5432/fuzefront_platform
+    DB_HOST: localhost
+    DB_PORT: '5432'
+    DB_NAME: fuzefront_platform
+    DB_USER: postgres
+    DB_PASSWORD: postgres
     NODE_ENV: test

Note: I prepared a commit with this exact fix on branch claude-auto-fix-ci-feat/db-bootstrap-least-privilege-27804676977 but the bot token lacks the workflows permission needed to push changes to .github/workflows/ci.yml. A human with write access will need to apply the diff above (or merge the branch after granting the permission).

@github-actions

Copy link
Copy Markdown
Contributor

CI fix pushed — branch claude-auto-fix-ci-feat/db-bootstrap-least-privilege-27804927827

Root cause

npm run test:coverage (jest --coverage) ran all test suites, including tests/permit-integration.test.ts. That file's import chain (organizations.tspermissions.tssrc/config/permit.ts) caused the module to throw at load time when PERMIT_API_KEY is not set:

PERMIT_API_KEY environment variable is required
  at Object.<anonymous> (src/config/permit.ts:20:9)

The "Generate test coverage" step in backend-tests.yml does not set PERMIT_API_KEY, so the entire suite failed before a single test ran. All 139 individual tests passed — only this suite-level load error caused exit code 1.

Fix

Added --testPathIgnorePatterns=permit-integration to the test:coverage script in backend/package.json, matching the flag already on test:integration. permit-integration.test.ts is a live test requiring a real Permit.io API key and PDP service which are intentionally absent from this CI job.

-"test:coverage": "jest --coverage",
+"test:coverage": "jest --coverage --testPathIgnorePatterns=permit-integration",

The fix is on branch claude-auto-fix-ci-feat/db-bootstrap-least-privilege-27804927827. A PR could not be opened automatically (GitHub Actions cannot create PRs in this repo), so please merge or cherry-pick from that branch.

@github-actions

Copy link
Copy Markdown
Contributor

CI Fix — branch pushed, PR creation blocked by token permissions

A fix has been pushed to branch claude-auto-fix-ci-feat/db-bootstrap-least-privilege-27804829937. The GitHub App token lacks the permissions needed to open a PR automatically; please open it manually targeting feat/db-bootstrap-least-privilege.


Root Cause

The Backend Authentication Tests job (test (20.x)) fails at Generate test coverage with:

FAIL tests/permit-integration.test.ts
  ● Test suite failed to run
    PERMIT_API_KEY environment variable is required
    at src/config/permit.ts:20

permit-integration.test.ts imports organizations.ts → permissions.ts → permit.ts, which throws at module-load time when PERMIT_API_KEY is unset. The Generate test coverage step in backend-tests.yml does not set that variable, so jest --coverage --runInBand fails the moment Jest tries to load the file.

The FATAL: role "root" does not exist lines visible in the error log tail are from the Postgres container's initialization (before the runner connects as postgres); they are cosmetic and unrelated to the failure.

Fix (backend/package.json)

-    "test:coverage": "jest --coverage",
+    "test:coverage": "jest --coverage --testPathIgnorePatterns=permit-integration",

The permit-integration tests require a live Permit.io API key and a running PDP service — neither available in this workflow. The existing test:integration script already excludes them with the identical flag (--testPathIgnorePatterns=permit-integration); this makes test:coverage consistent.

Note: modifying .github/workflows/backend-tests.yml directly was attempted first but rejected — the token lacks workflows write permission — so the fix was applied to the npm script instead.

@izzywdev
izzywdev merged commit 5166412 into master Jun 19, 2026
14 of 17 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

CI autofix findings — run 27804777382

Root cause: The Playwright e2e workflow's Postgres service container was missing POSTGRES_DB: fuzefront_platform.

After the bootstrap/runtime split in c9604fc, ensureDatabase() no longer issues CREATE DATABASE — it only verifies the database already exists. But the service container was starting without pre-creating fuzefront_platform, so the backend exited with:

❌ Error verifying database: Application database "fuzefront_platform" does not exist.
   It must be created by the privileged bootstrap step ...

This caused the backend health-check to time out after 120 s and all Playwright tests to be skipped.

Fix already applied: Commit c7f6358 (on this branch) adds POSTGRES_DB: fuzefront_platform to the e2e service container, which makes the Docker entrypoint pre-create the database before the health-check passes. That commit landed ~2 minutes after the failing run had already started, so the run captured the unfixed state.

Additional change pushed to claude-auto-fix-ci-feat/db-bootstrap-least-privilege-27804919519 (branch 773b589): improves the ensureDatabase() error message to explicitly mention both recovery paths (Helm bootstrap Job for Kubernetes; POSTGRES_DB=<name> for CI/local dev) so future misconfigurations are immediately actionable.

The next CI run on this PR should pass — the workflow fix is already in place on this branch.

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.

1 participant