fix(postgres): the user database names its Branch in the create#122
Closed
wmadden-electric wants to merge 7 commits into
Closed
fix(postgres): the user database names its Branch in the create#122wmadden-electric wants to merge 7 commits into
wmadden-electric wants to merge 7 commits into
Conversation
Deploy state moves from the workspace-level prisma-composer-state project
into a framework-owned database of the same name inside each stage's own
Branch; production's rides the Project's implicit default Branch. State then
has exactly the lifetime of the environment it describes: deleting a Branch
or Project — from the CLI, the Console, or any future git integration —
takes its state with it, with no knowledge of the framework on the platform
side.
ADR-0009 rejected this as circular ("the project is created and destroyed by
the deploys whose state it would hold"). ADR-0023/0024 dissolved that: the
CLI creates the containers via the Management API before Alchemy runs and
deletes them after destroy, outside state entirely. The remaining objection
(fragmentation of workspace-level questions) answers to the platform — those
are Project and Branch listings, not state queries.
ADR-0009 is marked superseded and annotated in the index; ADR-0010 records
that the lock now lives in a per-stage store (redundant key kept) and that
deleting the store severs the lease, killing an in-flight deploy of a stage
being torn down. ADR-0024 gains the distinction the PDP team confirmed: "the
default stage lives at the Project level" describes addressing, not physics
— the platform attaches every resource to a Branch, defaulting to the
Project's implicit one.
The sweep for prose describing the workspace store as current also reached
ADR-0011, ADR-0023, ADR-0030 (service-key values), layering.md's
provisioning-state spectrum, and alchemy-lowering.md. The deploying guide
gains the destroy ordering and a manual cutover procedure — there is no
automated migration.
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Two slices: this one (ADR-0033 and the documentation corrections) and the implementation of the per-branch bootstrap plus the destroy ordering the ADR commits us to. design-notes.md carries the implementation-grade design the second slice executes, including the platform facts it rests on, verified against pdp-control-plane rather than inferred. The plan also records the successor project — the Compute GitHub App integration for Composer repos — which this project is a prerequisite for: containment makes webhook-driven branch teardown correct, and project-scoped state is what lets a build hold a project-scoped credential instead of a workspace-scoped one. assets/pdp-asks.md is the standalone write-up of the two open questions for the PDP team (per-stage database quota/billing; the dependency contract of project deletion). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Both questions this project was going to ask the PDP team are answerable from their source, so they are answered rather than asked. The plan limit on databases is workspace-wide — counted across every project — so each stage's state store takes one of the workspace's 50 (free) or 1000 (paid) slots. Nothing bills per database: `createDatabase` carries a limit and never a price, and Postgres bills storage in GiB-hours and queries per cycle, both of which round to nothing for a few rows of JSON read only at deploy. The case that can bite is a free-plan workspace running many preview stages against the same 50 slots, so the ADR says that rather than "one extra small database, no big deal". The second answer corrects something I had wrong. Project deletion is blocked only by *active compute deployments*: deleteProject deletes every one of the project's database tenants itself, and deleteWithGuard refuses only on a live ComputeVersion, then cascades. Neither Branches nor databases ever block it — the schema comment on App.branchId says so outright. So the production state database was never going to wedge deleteAppProject the way I claimed; that step stays because production's state should not outlive production and hold a slot when another stage keeps the Project alive. The named-stage step is unaffected and genuinely is a precondition: DELETE /v1/branches does refuse a Branch with live members. The design notes now also point at the pdp-control-plane checkout in this worktree rather than the older one in ~/Projects — they had already diverged. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Implements ADR-0033. Bootstrap no longer hunts the workspace for a project named prisma-composer-state; it resolves the state database inside the Branch the CLI already resolved for the deploy, and destroy removes that database before the container it lives in. `prismaState()` now reads PRISMA_PROJECT_ID and (named stages only) PRISMA_BRANCH_ID — the ids run-alchemy already sets on the child process — so the store is addressed directly instead of discovered by listing every project in the workspace and inspecting same-named candidates. `PRISMA_WORKSPACE_ID` leaves the state layer entirely. Production passes no branch and the store resolves the project's default Branch read-only, by its isDefault flag: the platform guarantees every live project owns one, and creating it is the platform's business, not ours. The store's schema, its lock, its service, and the transient store are untouched, as is resource lowering. `discovery.ts` exists because bootstrap and delete must resolve the branch and list candidates identically — two copies would drift, and drifting apart is exactly how a delete removes something a bootstrap would have adopted. Ownership is verified by marker before adopting a database and again before deleting one. Deleting by name alone would take a user's database that happens to share the name, which is the guessing ADR-0005 forbids; delete.test.ts pins this with a stub that throws rather than a stub that returns. Destroy's two failure modes are deliberately different. A named stage throws: the Branch delete would fail anyway while a database is attached, so failing at the state step names the real cause. Production warns and continues, inheriting deleteAppProject's stance that failing a command over a cleanup step is worse than leaving a shell behind. Verified live against a real workspace, not only in tests: a named stage's state database lands on that stage's Branch and the round trip leaves no database, no Branch, and no Project; production's lands on the implicit default Branch and is removed before the Project; a stage whose Branch is torn down by the platform's cascading path loses its state with it and redeploys from a genuinely fresh store. No workspace-level state project is created by any path. That run also corrected the ADR. The platform has two Branch-delete paths, not one: the cascading teardown (git integration, idle-preview reclaim) removes the state database along with every other child, while the guarded delete the Console uses refuses outright while a database is attached. The consequences now say so instead of claiming every platform surface cascades. skills/prisma-composer/SKILL.md said state was workspace-hosted and shared by every deployer. It is neither, and it is a file agents read. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The design notes now carry what actually happened rather than what I first claimed: the flat database endpoint is a server-side create-then-attach and narrows the stray-database window rather than closing it; project deletion is blocked only by active compute deployments; there are two platform Branch-delete paths and only the cascading one cleans state up. Each was found by the implementer or the reviewer reading source I had asserted from a schema. Also recorded: the residual we accepted cannot be regression-tested with an honest fake, a failed deploy strands its containers because ensureContainers runs before preflight, and the follow-ups this slice deliberately left alone. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The state bootstrap takes 6.3 seconds on a stage's first deploy, not the minute-plus I claimed. pn-widgets runs 89s here against 92s on main — no measurable regression. Two e2e runs timed out at 3m22s and I read "52 seconds" as the state database provisioning. It was time from job start: checkout, install, build — work both branches do identically, before the deploy command runs at all. The real cause was a platform window that killed three branches between 12:18 and 12:26 (spi-inversion, this one, streams-minted-key) and cleared by 12:37, after which every run passed including this one. The commit that carried the invented cost into ADR-0033's consequences and raised the CI budget to accommodate it is dropped rather than reverted, so the claim never enters the record. The budget stays at 3 minutes. That the budget is marginal is true and worth its own change, argued from the degradation-window data rather than from a misdiagnosis. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
A database created without a branchId is born on the project's default Branch, never branchless: createPrismaPostgresDatabase calls getOrCreateDefaultBranch inside its create transaction. So the create-then-PATCH two-step put a named stage's database on production's Branch first and only moved it on the PATCH — and a failed PATCH, or a CLI that died between the two calls, stranded a user's database on production's Branch. The flat POST /v1/databases takes projectId, name, region, isDefault and branchId in one request and validates the Branch before creating the row; the project-scoped route takes no branch parameters at all. The platform still attaches in a second step of its own, so this narrows the window rather than closing it — a failed attach can still leave a stray database, which costs a quota slot rather than correctness. The PATCH stays on the observed path, where there is no create to strand anything and it is what moves a drifted database back onto its Branch. ComputeService and EnvironmentVariable already name the Branch in their create bodies and needed no change. Supports ADR-0033. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
wmadden-electric
force-pushed
the
claude/compute-app-containment-model-179b9a
branch
2 times, most recently
from
July 17, 2026 15:24
04cb231 to
0ad59f4
Compare
Base automatically changed from
claude/compute-app-containment-model-179b9a
to
main
July 17, 2026 15:31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacks on #113. The two-step create in
postgres/Database.ts—POST /v1/projects/{projectId}/databasesthenPATCH /v1/databases/{databaseId}{ branchId }— is the same shape ADR-0033 removed from the state database, and it carries the same unsafe window for user databases. #113 filed it as a known latent bug and left it out of scope; this closes it.Why the two-step is unsafe
A database created without a
branchIdis born on the project's default Branch, never branchless —createPrismaPostgresDatabasecallsgetOrCreateDefaultBranchinside its create transaction. ADR-0033 records the same platform fact (line 46). So on a named-stage deploy the two-step put the user's database on the default Branch — production's — and only moved it on thePATCH. A failedPATCH, or a CLI that died between the two calls, stranded a user database on production's Branch.The fix
Use the flat
POST /v1/databases, which takesprojectId,name,region('inherit'supported),isDefault, andbranchIdin one request and validates the Branch before creating the row. This is whatstate/bootstrap.ts'screateStateDatabasedoes — the reference for this change.The flat endpoint is not atomic: the platform still creates the row and attaches the Branch in two server-side steps. This narrows the window — one request, Branch validated before the row exists, no client-crash window between two calls — it does not close it. A failed attach can still leave a stray empty database, which costs a quota slot rather than correctness: the same residual #113 accepted for the state database. The comment in the code says exactly this and does not claim atomicity.
The
PATCHstays on the observed path only, where there is no create to strand anything and it is what moves a database that drifted off its Branch back onto it.Scope checked
compute/ComputeService.tsandcompute/EnvironmentVariable.tsalready name the Branch in their create bodies — ComputeService with a comment explaining why — so neither needed a change. Onlypostgres/Database.tshad the two-step.Verified
pnpm typecheckclean (which is what proves the flat body conforms to the SDK'spostV1Databasesschema,branchIdincluded), 123/123 lowering tests pass, cast ratchetdelta=0. The tests were confirmed to catch the regression, not merely pass: reverting the source alone fails 3 of 5, and the fake client now throws on the project-scoped route so reaching for it fails loudly.One follow-up for #113's description
#113's "Known and accepted" section still says
postgres/Database.ts"carries the same two-step … Out of scope … filed." Merging this makes that false. Worth updating that bullet to point here.🤖 Generated with Claude Code