Skip to content

feat(orchestrate): provision saga phase — per-project Postgres role+db (D8) — M2 capstone - #73

Merged
gustavobertoi merged 1 commit into
mainfrom
nightly/provision-phase
Jun 29, 2026
Merged

feat(orchestrate): provision saga phase — per-project Postgres role+db (D8) — M2 capstone#73
gustavobertoi merged 1 commit into
mainfrom
nightly/provision-phase

Conversation

@gustavobertoi

Copy link
Copy Markdown
Contributor

What

Wires the long-deferred provision phase into the up saga — the M2 capstone. Each active project that uses: workspace.shared.postgres gets its own login role + owned database, created idempotently via internal/provision (existence-guarded pgx SQL, DECISIONS D8). Unblocks X3 firstRun.

The host-port / determinism solution

Provisioning runs pgx from the host, so shared Postgres needs a reachable port. Instead of publishing it in the deterministic, golden-asserted generated compose (which would also flip "no host ports by default" for everyone), the shared phase writes an up-time overlay .devstack/shared/compose.provision.yaml mapping 127.0.0.1:<ledger port>:5432 and brings the stack up with -f compose.yaml -f compose.provision.yaml.

  • generate output is untouchedmake determinism still byte-identical ✅
  • the port is loopback-only (nothing exposed beyond the host)
  • docker.Compose gained an Overrides field for the extra -f

Flow

shared (postgres healthy, port published) → provision (pgx-connect to 127.0.0.1:<port> as the template admin, EnsureProject per project, record role+db ownership in the ledger under the flock) → per-project compose-up.

The host port is ledger-allocated (Manager.FreeHostPort, purpose pg-provision) and re-derived idempotently by the provision phase (survives re-runs/crashes); the shared fingerprint folds in the provisioned set so adding a consumer re-publishes the port.

Credential default (non-overriding)

Per-project password = the project name — a predictable credential for a loopback-only, network-isolated dev DB (THREAT-MODEL: container isolation is a non-goal). Nothing secret is generated or stored; an app opts in via postgres://<project>:<project>@shared-postgres:5432/<project> and devstack never overrides an app's own DB env. --no-provision opts out.

Tests (daemon-free via an injectable PgConnect seam)

Full saga provisions app's role+db (CREATE ROLE/DATABASE create path), connects on loopback, records ownership in the ledger, brings shared up with the overlay (loopback-bound to 5432); --no-provision skips the phase. make ci + make determinism + integration-build green.

Spec acceptance (spec 03 / D8)

  • Per-project role+db on the shared Postgres, idempotent (existence-guarded), under the flock.
  • No host ports by default elsewhere; the provisioning port is loopback-only + ledger-allocated.
  • Generation stays byte-deterministic (overlay is up-time, not generated).

🤖 Generated with Claude Code

…b (D8) — M2 capstone

Wires the long-deferred provision phase into the up saga. Per-project data
isolation on the shared Postgres (spec 03, DECISIONS D8): each active project that
`uses: workspace.shared.postgres` gets its own login role + owned database, created
idempotently via the existing `internal/provision` (existence-guarded pgx SQL).

Determinism-safe host port: provisioning runs pgx FROM THE HOST, so the shared
Postgres needs a reachable port. Rather than publish it in the deterministic,
golden-asserted generated compose (which would also flip the "no host ports by
default" posture for everyone), the shared phase writes an UP-TIME overlay
(`.devstack/shared/compose.provision.yaml`) mapping `127.0.0.1:<ledger port>:5432`
and brings the shared stack up with `-f compose.yaml -f compose.provision.yaml`.
`generate` output is untouched (CI determinism still byte-identical) and the port
is loopback-only. `docker.Compose` gained `Overrides` for the extra `-f`.

Flow: shared (postgres healthy, port published) → provision (pgx-connect to
127.0.0.1:<port> as the template admin, EnsureProject per project, record
role+db ownership in the ledger under the flock) → per-project compose-up. The
host port is ledger-allocated (`Manager.FreeHostPort`, purpose `pg-provision`) and
re-derived idempotently by the provision phase, so it survives re-runs/crashes; the
shared fingerprint folds in the provisioned set so adding a consumer re-publishes.

Dev-credential default: the per-project password is the project name — a
predictable credential for a loopback-only, network-isolated dev DB (THREAT-MODEL:
container isolation is a non-goal), so nothing secret is generated/stored and an
app opts in via `postgres://<project>:<project>@shared-postgres:5432/<project>`
(never auto-overriding the app's own DB config). `--no-provision` opts out.

Tested daemon-free via an injectable `PgConnect` seam: the full saga provisions
app's role+db (CREATE ROLE/DATABASE on the create path), connects on loopback,
records ownership, brings shared up WITH the overlay (loopback-bound to 5432), and
`--no-provision` skips the phase entirely. `make ci` + `make determinism` green.

Unblocks X3 firstRun (provision scope_key now exists).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gustavobertoi
gustavobertoi merged commit 8eb96e8 into main Jun 29, 2026
3 checks passed
@gustavobertoi
gustavobertoi deleted the nightly/provision-phase branch June 29, 2026 20:09
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