Skip to content

feat: deploy a stage as an isolated Prisma Cloud Branch (ADR-0022/0023) - #42

Merged
wmadden-electric merged 21 commits into
mainfrom
claude/stage-as-branch
Jul 12, 2026
Merged

feat: deploy a stage as an isolated Prisma Cloud Branch (ADR-0022/0023)#42
wmadden-electric merged 21 commits into
mainfrom
claude/stage-as-branch

Conversation

@wmadden-electric

@wmadden-electric wmadden-electric commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

What

prisma-app deploy --stage <name> now provisions the whole topology into an isolated Branch of the app's single Prisma Cloud Project — its own compute, database, config, and Alchemy state — so an app can run production plus staging plus per-PR previews side by side.

Implements ADR-0022 (a Prisma App is one Project; a Stage is a Branch) and ADR-0023 (a stage is a deploy-time environment; the CLI resolves Project + Branch outside Alchemy before the stack runs).

Model

  • A Prisma App = one Project. Its Systems become Apps (compute services) + Databases inside that Project.
  • A Stage = a Branch. No --stage → the project-level production environment (unchanged). --stage X → an isolated Branch X.
  • Two-phase deploy: the CLI ensures the Project and Branch via the Management API before Alchemy, injecting PRISMA_PROJECT_ID / PRISMA_BRANCH_ID into the alchemy child; the target then provisions resources within them. Neither container is an Alchemy resource (a Branch can't live inside the state namespace named after it; the Project is shared across all stages).
  • Config class = branchId ? 'preview' : 'production', mechanical — never a role lookup.

Changes

  • @prisma/alchemyresolveContainer (adopt-oldest-by-name Project resolution + create-if-absent Branch, ensure flag for find-only destroy); deleteBranch; Database/ComputeService gain branchId (Database attaches via PATCH, ComputeService via the create body — see note).
  • @prisma/app-cli — resolves + injects the ids for deploy and destroy; destroy requires an explicit target (--stage <name> or --production; a bare destroy errors, so an omitted stage can never silently tear down production); git-ref stage-name validation; soft-deletes the named-stage Branch after teardown.
  • @prisma/app-cloud — the target references the injected Project instead of minting one, and threads branchId/class through every resource.
  • Docs — ADR-0022/0019, the slice spec/plan, and a design-decisions log.

Proven live

Deployed examples/storefront-auth against a real workspace:

Step Result
deploy (production) 24 resources on the main branch
deploy --stage staging isolated staging branch — own compute + own empty Postgres
staging auth.verify() {ok:true} (reaching its own DB)
staging config all env vars class:preview + branchId
re-deploy no-op (idempotent)
destroy --stage staging staging Branch soft-deleted; production untouched + still {ok:true}
destroy --production prod resources gone; main branch survives

The live test also caught a real bug missed by unit tests + code review: compute-service names are unique per Branch, so a project-scoped create collided with production on main — fixed by creating on the Branch via the create body (design-decision #3).

Note

Supersedes #37 — that PR's ADR-0016 is the same "App=Project, Stage=Branch" decision, renumbered here to ADR-0022 (main independently took ADR-0016–0021 after that branch forked). #37 is being closed as superseded.

🤖 Generated with Claude Code

@wmadden-electric
wmadden-electric force-pushed the claude/stage-as-branch branch from 60e357f to acd50a4 Compare July 11, 2026 17:51
@wmadden-electric wmadden-electric changed the title feat: deploy a stage as an isolated Prisma Cloud Branch (ADR-0018/0019) feat: deploy a stage as an isolated Prisma Cloud Branch (ADR-0022/0023) Jul 11, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jul 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma/alchemy

npm i https://pkg.pr.new/@prisma/alchemy@42

@prisma/app

npm i https://pkg.pr.new/@prisma/app@42

@prisma/app-assemble

npm i https://pkg.pr.new/@prisma/app-assemble@42

@prisma/app-cli

npm i https://pkg.pr.new/@prisma/app-cli@42

@prisma/app-cloud

npm i https://pkg.pr.new/@prisma/app-cloud@42

@prisma/app-nextjs

npm i https://pkg.pr.new/@prisma/app-nextjs@42

@prisma/app-node

npm i https://pkg.pr.new/@prisma/app-node@42

@prisma/app-rpc

npm i https://pkg.pr.new/@prisma/app-rpc@42

prisma-app

npm i https://pkg.pr.new/prisma-app@42

commit: ce59644

@wmadden wmadden left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Preemptively approved. Fix comments first

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This deserves to be documented in the domains/ documentation. We're introducing important, long-lived concepts: a stage (which equates to a Branch on PDP) and in the Alchemy domain, we're altering its state store logic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Documented in 4a9d533: 10-domains/deploy-cli.md gains the container-resolution pipeline step and a "Stages and containers" section (explicit-destroy contract included); 03-domain-model/glossary.md gains Stage and Container — Project/Branch entries; the Alchemy-domain facts landed in 05-prisma-cloud/alchemy-lowering.md (Branch-per-stage placement, the resolveContainer/deleteBranch client and its reuse of the state-bootstrap idiom, and state keying unchanged — Project = stack, Branch = stage); two stale claims in pdp-data-model.md ("only ever touches the production branch", "future stages story") corrected. This ADR is now ADR-0024. On "altering its state store logic": the state store itself is unchanged by this slice — @prisma/alchemy gained the sibling container-resolution client, which shares the store bootstrap's client and adopt-oldest idiom; the docs say exactly that.

result = { id: observed.data.id, name: observed.data.name };
} else {
const created = yield* call(() =>
client.POST('/v1/projects/{projectId}/databases', {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why aren't we using the Prisma management API client/sdk?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We are — client here is @prisma/management-api-sdk's createManagementApiClient (its typed openapi-fetch surface), constructed once from PRISMA_SERVICE_TOKEN in packages/alchemy/src/client.ts and provided to every provider through the ManagementClient service. The client.GET/POST/PATCH('/v1/…', { params }) calls are that SDK's calling convention (path-template + typed params), not hand-rolled HTTP.

env: input.env ?? process.env,
env: {
...(input.env ?? process.env),
PRISMA_PROJECT_ID: input.projectId,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This looks like Prisma CLoud config leaking into the framework

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed — the PRISMA_PROJECT_ID/PRISMA_BRANCH_ID names here (and the resolveContainer import in ensure-containers.ts) are Prisma Cloud specifics living in the otherwise target-agnostic CLI. It is the accepted MVP shape of ADR-0024's "CLI ensures containers" decision. Recorded the durable fix as a Drive deferred item in this PR (68d7d77, .drive/deferred.md): a deploy-plane hook on the extension descriptor — the extension resolves its own containers and supplies the child env; the CLI stays generic.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Both these ADRs are walls of text, impenetrable to the poor reader

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Rewritten in 7a05ac0 (and its pair in the same commit). Both ADRs now open with a grounding example — this one the system() snippet plus the Workspace → Project → Branch tree it lowers to — lead with the decision, and end with alternatives. Cut as transient: the "current mapping / remaining work" pre-implementation snapshot and the Open Questions section (every question in it had been answered by the companion ADR). ~40% shorter. Note: renumbered to ADR-0023 — main took 0022 (data-deps contract) while this was in review.

wmadden-electric and others added 17 commits July 12, 2026 09:25
…age-as-branch spec/plan

Renumbered off current main (my prior 0016/0017 collided with main-side
ADRs added since the fork). ADR-0018: a Prisma App is one Project, a Stage is
a Branch. ADR-0019: a stage is a deploy-time environment; the CLI resolves the
Project (by root-system name) and Branch outside Alchemy. Project-id override
reuses the prisma-app.config.ts from ADR-0017. Plus the fully-pinned slice
spec and 4-dispatch plan for the first implementation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Adds resolveContainer() to @prisma/alchemy: given a workspace id, app
name, and optional stage, it finds-or-creates the app's Project
(adopt-oldest among duplicate names, per ADR-0019/ADR-0018) and, for a
named stage, finds-or-creates its Branch by gitName. The default stage
creates no Branch. Both use the existing ManagementClient; Branch
create-if-absent is idempotent client-side (observe, then re-observe
on a racing 409) since the Management API has no ifExists parameter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
D1 found the Management API has no ifExists field on POST branches: it
accepts only gitName + isDefault and 409s on a duplicate. Spec §4 and plan
D1 assumed server-side create-or-return. Rewrite both to the real mechanism
(observe via GET ?gitName= exact-match, POST when absent, re-observe on a
racing 409) and record the falsified assumption in design-decisions.md.
Outcome unchanged; only the mechanism was mis-specified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
alchemy destroy re-evaluates the same stack, so once D3 makes projectId
required the destroy child needs the ids too. Pin: both deploy and destroy
inject PRISMA_PROJECT_ID/PRISMA_BRANCH_ID; destroy resolves find-only
(resolveContainer gains ensure:boolean) and never creates a container; a
missing container is a clear "nothing deployed" error. Named-stage Branch
soft-delete (DELETE /v1/branches/:id after members are gone) is pinned to D4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…c §10)

A bare `destroy` defaulting to production is a footgun: an omitted or
typo'd --stage could tear down prod. Require an explicit target — --stage
<name> for a branch, or --production for the project-level env; bare destroy
and --stage+--production both error; --production is destroy-only. deploy is
unchanged. Internals unchanged: --production is just stage=undefined find-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Before running the Alchemy stack, the CLI now resolves the app's Project and (for a named stage) Branch via the Management API, then injects PRISMA_PROJECT_ID/PRISMA_BRANCH_ID on the alchemy child for both deploy and destroy. Deploy creates-if-absent; destroy resolves find-only and fails clearly if nothing was deployed.

Also enforces spec section 10's explicit-destroy-target rule (added mid-implementation): destroy no longer silently defaults to production — it requires --stage <name> or --production, mutually exclusive, with --production destroy-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…d at lowering time

Grounding D3 against @prisma/management-api-sdk falsified two spec pins:
(1) the db/compute create bodies do NOT accept branchId — attachment is a
PATCH after create (EnvironmentVariable does take branchId+class in its body);
(2) the parent evaluates prismaCloud() at config-load before ensureContainers
computes the ids, so projectId cannot be required at construction — it is read
at lowering time in application.provision. Rewrote spec §2/§5/§6/§7, logged
decision #2, split D3 into D3a (providers) + D3b (target).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Adds an optional branchId prop to both providers. When set, reconcile PATCHes the resource onto that Branch after observe-or-create, on every reconcile, so a named-stage deploy is self-healing and idempotent. When unset (default/production stage), no PATCH is issued — production behavior is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
application.provision now references PRISMA_PROJECT_ID (set by the CLI) instead of minting a Project. When PRISMA_BRANCH_ID is set (a named stage), the Database, ComputeService, and every EnvironmentVariable it writes carry that branchId, and env vars switch to class: preview. Default-stage behavior is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
… (D3 review)

The D3 Opus review verified against the SDK that the compute-service create
body accepts branchId (line 8628); only the database create lacks it. Code is
unchanged — both providers use the uniform PATCH mechanism (a harmless extra
idempotent call for compute). Record Connection/Deployment inherit the branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
D4a: after a successful alchemy destroy on a named stage, the CLI now calls DELETE /v1/branches/{branchId} to soft-delete that stage's Branch. The production environment has no branch and is never touched; a failed destroy or a deploy never triggers the delete.

Adds deleteBranch to @prisma/alchemy (tolerates a 404), deleteStageBranch to app-cli (surfaces API refusals as CliError), and wires it into main.ts's step-9 alchemy block via a new RunDeps.deleteBranch seam.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
A live deploy hit compute_service:already_exists on branch "main": a project-scoped
create always lands on the default Branch, and compute-service names are unique per
Branch, so it collided with the same-named production service before the PATCH ran.

ComputeService now puts branchId in the create body and drops the PATCH entirely;
Database is unaffected (its create body has no branchId, so it stays create-then-PATCH).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Main advanced to include ADR-0018/0019/0020/0021 (config-schema decisions),
so the App=Project / Stage=Branch ADRs renumbered from 0018/0019 to 0022/0023.
Fixes the spec references to match the renamed ADR files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Ground each ADR in a concrete example (the system() snippet and the CLI
invocations + a two-phase deploy diagram), lead with the decision, end with
alternatives. Remove transient content: pre-implementation snapshots, refactor
archaeology, deferred/roadmap notes, and open questions ADR-0023 already
answered. Fix drift vs shipped behavior (no ownership marker, default stage
creates no Branch, class is computed mechanically) and record the explicit
destroy-target contract, which was missing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Fourth collision: origin/main gained ADR-0022 since the last rebase. App=Project
becomes ADR-0023, stage→environment becomes ADR-0024; all cross-refs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…iew)

The CLI hard-codes Prisma Cloud specifics (resolveContainer import, the
PRISMA_PROJECT_ID/PRISMA_BRANCH_ID child env) — target config leaking into the
target-agnostic CLI. Accepted for this slice per ADR-0024; durable fix is a
deploy-plane hook on the extension descriptor. From the run-alchemy.ts review
comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Document the stage-as-branch surface: the deploy CLI's stage/container resolution pipeline and explicit destroy contract in deploy-cli.md, the Stage/Container glossary terms, and the Project/Branch container-resolution client plus unchanged state keying in alchemy-lowering.md and pdp-data-model.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric
wmadden-electric force-pushed the claude/stage-as-branch branch from acd50a4 to 4a9d533 Compare July 12, 2026 07:37
wmadden-electric and others added 4 commits July 12, 2026 09:43
…production

CI destroy-guard.sh used the old bare `destroy` form; the CLI now requires
an explicit target, so it errors. Point it at --production, since CI
deploys the production environment of a per-run stack name.

The pipeline resolved containers (a cloud mutation) before assembly (local
validation), so a deploy that could not assemble still created a Project/
Branch. Reordered so assembly runs first — local validation before cloud
mutation — fixing the integration test that pins the "no built entry at"
error contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Adds the user-facing skill for the prisma-app deploy CLI: deploy to production or a named --stage environment, and the destroy command's requirement to name an explicit target (--stage or --production).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
destroy requires an explicit target; the package script still used the bare form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Long-lived doc: state the destroy contract as present-tense fact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric
wmadden-electric merged commit 82aea7a into main Jul 12, 2026
13 checks passed
wmadden-electric added a commit that referenced this pull request Jul 12, 2026
Main landed stage-as-branch (#42): the CLI resolves containers (Project +
optional stage Branch) and injects PRISMA_PROJECT_ID/PRISMA_BRANCH_ID; the
extension's application hook references that Project instead of minting one,
and branch-scoped resources (Database, ComputeService, EnvironmentVariable)
carry branchId with env vars classed preview on a stage / production
otherwise. destroy now requires an explicit --stage/--production target.

The one content conflict was packages/app-cloud/src/control.ts: main changed
the inline lowerings while this branch had extracted them to src/controls/*
(review R3). Resolution keeps the extracted structure and carries main's
semantics into it — resolveOptions reads projectId/branchId into
ResolvedCloudOptions (controls/shared.ts, with the required-at-provision
check staying in the application hook), the Database lowerings
(controls/postgres.ts AND controls/prisma-next.ts — the pn lowering gets the
same branch scoping) and ComputeService/serialize (controls/compute.ts) gain
the branchId/preview handling. control-lowering.test.ts auto-merged clean:
main's stage assertions and this branch's PgWarm stubs coexist (32 pass).
pnpm-lock.yaml regenerated via pnpm install.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
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