diff --git a/.drive/projects/stage-as-branch/design-decisions.md b/.drive/projects/stage-as-branch/design-decisions.md deleted file mode 100644 index 33f39bf98..000000000 --- a/.drive/projects/stage-as-branch/design-decisions.md +++ /dev/null @@ -1,100 +0,0 @@ -# Design decisions — stage-as-branch slice - -Numbered log of mid-flight decisions that amend the spec/plan. Each records the -trigger, what was learned, the decision, and the affected artefacts (Drive I12). - -## 1. Branch idempotency is client-side; the API has no `ifExists` field - -- **Trigger:** falsified assumption, found during D1 implementation. Spec §4 and plan - D1 described creating a Branch "via `POST /v1/projects/:id/branches` (`ifExists: - "return"`)" as if server-side create-or-return idempotency existed. -- **Learned:** it does not. Verified against `@prisma/management-api-sdk@1.47.0` (and - the live OpenAPI): `POST /v1/projects/{projectId}/branches` accepts only `gitName` + - `isDefault` (`additionalProperties: false`); a duplicate `gitName` returns `409`. The - matching read, `GET …/branches?gitName=X`, **is** a real server-side exact-match - filter returning ≤1 row. -- **Decision:** keep the spec's *outcome* (idempotent create-if-absent keyed by - `gitName`) and implement idempotency client-side: observe via `GET ?gitName=`, `POST` - only when absent, and on a racing `409` re-observe and adopt the winner. This mirrors - the adopt-oldest/tolerate-races idiom already in `state/bootstrap.ts`. The mechanism - changed; no architectural decision changed. -- **Affected:** spec §4 (rewritten), plan D1 (rewritten), `packages/alchemy/src/container.ts` - (`resolveBranch`). Confirms spec §4's positional-role note: the API doc states the first - Branch is `role=production`, later Branches `role=preview`, server-owned regardless of - body — so explicit role control stays deferred. - -## 2. Branch attachment is a PATCH, and ids are read at lowering time (not `PrismaCloudOptions`) - -- **Trigger:** falsified assumption, found while grounding D3. Spec §6 said the `Database`/ - `ComputeService` **create bodies accept `branchId`**; spec §2/§7 said the ids arrive via a - new `PrismaCloudOptions.projectId`/`fromEnv()`. -- **Learned (verified against `@prisma/management-api-sdk@1.47.0`):** - 1. `POST /v1/projects/:id/databases` create body has **no `branchId`** — a database is created - project-scoped and **attached to a Branch by `PATCH /v1/databases/:id` with `{ branchId }`**. - (`.../compute-services` create *does* accept `branchId`, but D3a uses the same PATCH mechanism - for both providers for uniformity — a harmless extra idempotent call for compute. Corrected - from an earlier claim that neither create body accepted it — the D3 Opus review caught it - against SDK line 8628.) `EnvironmentVariable`'s create body accepts `branchId` + `class` - directly. `Connection`/`Deployment` are not branch members (they inherit via their parent). - 2. There is no `fromEnv()`/`PrismaCloudOptions` id path today — the target reads env in - `resolveOptions`. And the CLI evaluates `prismaCloud()` in the **parent** at config-load - (before `ensureContainers` computes the ids), so `projectId` **cannot be required at - construction** — it must be read at **lowering time** in `application.provision` (child - only). -- **Decision:** (a) providers gain an optional `branchId`; when set they PATCH-attach the - resource after observe-or-create on **every** reconcile (idempotent, self-healing); unset = - no PATCH = current behavior. (b) `resolveOptions` reads `PRISMA_PROJECT_ID`/`PRISMA_BRANCH_ID` - **without requiring them**; the required check for `projectId` lives in `application.provision`. - No `PrismaCloudOptions` field is added (config-file override deferred). Outcome (branch-isolated - resources + `class` mechanical) is unchanged; only the mechanism was mis-specified. -- **Affected:** spec §2/§5/§6/§7 (rewritten); plan D3 split into **D3a** (providers, `@prisma/ - alchemy`) → **D3b** (target, `@prisma/app-cloud`); `packages/alchemy/src/postgres/Database.ts`, - `.../compute/ComputeService.ts`, `packages/app-cloud/src/control.ts`. - -## 3. Compute-service create-then-PATCH collides with production on a live deploy - -- **Trigger:** mid-flight obstacle, found on a real `prisma-app deploy --stage staging`. -- **Symptom:** the create step of `ComputeService.reconcile` failed outright: - `compute_service:already_exists: An app named "auth" already exists on branch "main"`. - The PATCH that was meant to attach the service to the staging Branch never ran — the - preceding create was rejected. -- **Root cause:** decision #2 above put `ComputeService` on the same project-scoped - create-then-PATCH mechanism as `Database`, treating the extra PATCH as "harmless." It - is not harmless: compute-service names are unique **per Branch**, not per project. A - project-scoped `POST /v1/projects/:id/compute-services` (no `branchId`) always lands on - the project's default (`main`) Branch. If a same-named service already exists there — - here, the production `auth` service — the create collides with it before the PATCH step - is ever reached. -- **Decision:** `ComputeService` stops using PATCH. It passes `branchId` directly in the - create body (`POST /v1/projects/:id/compute-services`, body `{ displayName, regionId?, - branchId? }`), creating the service on the target Branch from the start — no collision, - no PATCH. Re-verified the create body accepts `branchId`: SDK - `postV1ProjectsByProjectIdCompute-services`, `index.d.ts:8628`. `Database` is unaffected - and stays exactly as decision #2 (create body has no `branchId`, so it must stay - create-then-PATCH) — this was verified working on the same live deploy. -- **Affected:** spec §6 (rewritten — providers no longer share one PATCH mechanism); - `packages/alchemy/src/compute/ComputeService.ts` (`reconcile`, drops the PATCH branch); - `packages/alchemy/src/__tests__/ComputeService.test.ts` (reconcile tests rewritten for - create-body `branchId`, no PATCH). - -## 4. Container resolution must run after assembly, not before - -- **Trigger:** the integration test (CI) falsified the ensure-before-assemble order - set by D2. `prisma-app deploy — real extension-config resolution of prisma-cloud + - node > resolves both /control entries for real and fails at the missing built - entry, not at resolution` deploys an unbuilt fixture and expects the pipeline to - fail with the "no built entry at" assembly error. It instead failed earlier, on - the missing `PRISMA_SERVICE_TOKEN`, because `ensureContainers` ran before - `assembleServices`. -- **Learned:** running container resolution before assembly lets a deploy that - cannot assemble mutate Prisma Cloud first — creating a Project and/or Branch for - a service that then fails to build. That regresses the established "no built - entry at" error contract this test pins, and it means a broken local build can - still leave a container behind in the cloud. -- **Decision:** assemble first (local validation, no cloud calls), then resolve - containers (the first cloud mutation), then generate and run the stack. A deploy - that cannot assemble now fails before anything is created in Prisma Cloud. -- **Affected:** `packages/app-cli/src/main.ts` (`run()`, steps renumbered — assemble - is now step 6, container resolution step 7); `docs/design/10-domains/deploy-cli.md` - (pipeline steps 5/6 swapped and renumbered to match); the integration test is - unchanged and now passes. diff --git a/.drive/projects/stage-as-branch/plan.md b/.drive/projects/stage-as-branch/plan.md deleted file mode 100644 index 34dc9699d..000000000 --- a/.drive/projects/stage-as-branch/plan.md +++ /dev/null @@ -1,93 +0,0 @@ -# Slice Plan — Deploy an app to a named stage as an isolated environment - -Spec: [`./spec.md`](./spec.md). Dispatches are sequential. Every design choice is in the -spec's **Pinned decisions**; a dispatch implements them, it does not decide them. Gate -for each code dispatch: `pnpm typecheck` + the touched package's tests; `pnpm build` on D3. - -## Dispatch 1 — Container-resolution client (`@prisma/alchemy`) - -**Outcome.** A client function, given the workspace token, returns -`{ projectId, branchId? }` by: (a) listing live Projects by name, adopting the oldest or -creating one (spec §3); (b) for a named stage, create-if-absent a Branch — observe via -`GET /v1/projects/:id/branches?gitName=X` (exact match), `POST` only when absent, tolerate -a racing `409` by re-observing (spec §4; the API has no `ifExists` field). For the default -stage it returns `branchId: undefined` and creates no Branch. - -- **Builds on:** the existing `ManagementClient` (already does authed Management-API - calls for `Project`/`EnvironmentVariable` providers). -- **Hands to:** D2 — a resolver returning the ids. -- **Focus:** client + resolver logic; Management-API HTTP mocked in tests. No CLI wiring. - Do **not** add an ownership marker or `--project` handling (spec: deferred). - -## Dispatch 2 — CLI ensure-containers step (`@prisma/app-cli`) - -**Outcome.** Before running the stack, the pipeline (for **both** deploy and destroy): -enforces the **explicit-destroy-target** rule (spec §10) — bare `destroy` errors; `destroy` -takes `--stage ` or `--production`, mutually exclusive; `--production` is destroy-only; -validates a named stage against git `check-ref-format` (fail clearly if invalid, spec §4); -calls the D1 resolver with the app name (root system name or `--name`) and the stage — -`ensure: true` for deploy (create-if-absent), `ensure: false` for destroy (find-only, -clear "nothing deployed" error if absent, spec §10); and sets `PRISMA_PROJECT_ID` (always) -and `PRISMA_BRANCH_ID` (named stages only) on the `alchemy` child (spec §2). Default -`prisma-app deploy` from a fresh checkout still works with only a token. Adds the `ensure` -option to `@prisma/alchemy`'s `resolveContainer` and surfaces the Management-client layer -from its barrel so the CLI can provide `ManagementClient` over `PRISMA_SERVICE_TOKEN`. - -- **Builds on:** D1. -- **Hands to:** D3 — an `alchemy` invocation carrying the ids in its env. -- **Focus:** `run-alchemy.ts` (child env) + the pre-stack step in `main.ts`; stage-name - validation; the `ensure` flag + barrel export in `@prisma/alchemy`. No provider or - target-lowering changes. Branch soft-delete on destroy is **D4**, not here. - -## Dispatch 3a — Providers gain a `branchId` (PATCH-attach) (`@prisma/alchemy`) - -**Outcome.** `Database` and `ComputeService` providers gain an optional `branchId` prop -(spec §6). The Management-API **create bodies do not accept `branchId`**, so when `branchId` -is set the provider **PATCHes** the resource to the Branch (`PATCH /v1/databases/:id` / -`PATCH /v1/compute-services/:id` with `{ branchId }`) after observe-or-create, on **every** -reconcile (idempotent). When unset, no PATCH — byte-for-byte current behavior. Backward- -compatible: `branchId` optional, undefined = today. - -- **Builds on:** D2. -- **Hands to:** D3b — providers that accept a Branch assignment. -- **Focus:** `packages/alchemy/src/postgres/Database.ts`, `.../compute/ComputeService.ts` + - their tests (mocked HTTP: assert PATCH fires when `branchId` set, and does NOT when unset). - No `EnvironmentVariable` change (its create body already takes `branchId`+`class`). No - `control.ts` change. - -## Dispatch 3b — Target consumes the ids (`@prisma/app-cloud`) - -**Outcome.** `resolveOptions` reads `PRISMA_PROJECT_ID`/`PRISMA_BRANCH_ID` from env **without -requiring them** (spec §2 — parent constructs before the ids exist). `application.provision` -stops calling `Prisma.Project(...)`; it reads `process.env['PRISMA_PROJECT_ID']` (**required -here**, clear error if absent) and emits it as its `projectId` output (spec §7). Every -resource site — the postgres `Database`, the compute `ComputeService`, and every -`EnvironmentVariable` (poison vars + serialized config) — passes `branchId` **iff** set and -sets config `class = branchId ? 'preview' : 'production'` (spec §5), removing the hardcoded -`production`. - -- **Builds on:** D3a. -- **Hands to:** D4 — a target that provisions into `(Project, Branch?)`. -- **Focus:** `packages/app-cloud/src/control.ts` + `__tests__/control-*.test.ts`. Pure - retargeting + the `branchId`/`class` conditional; no new provisioning logic, no migrations. - -## Dispatch 4 — Prove it live: a second environment - -**Outcome.** Against real Prisma Cloud (needs `.env` in the worktree root): -`prisma-app deploy` (production) then `prisma-app deploy --stage staging` for -`examples/storefront-auth` stand up two isolated environments; staging ingress → -`auth.verify()` returns `{ ok: true }`; re-deploy of each is a no-op; -`destroy --stage staging` removes the staging resources, then **soft-deletes the staging -Branch** via `DELETE /v1/branches/:branchId` (spec §10) — production untouched. - -- **Builds on:** D1–D3. -- **Hands to:** — (slice DoD). -- **Focus:** the branch soft-delete teardown step (spec §10) + end-to-end proof + destroy. - Report (do not fix) any gap in per-branch state segregation or Management-API branch/role - behavior as follow-up. - -## Notes - -- D3 is the largest; if the provider `branchId` addition and the `target.ts` conditional - want separate landings, the orchestrator splits at the plan-loop — that is an - orchestration call, not an implementer design choice. diff --git a/.drive/projects/stage-as-branch/spec.md b/.drive/projects/stage-as-branch/spec.md deleted file mode 100644 index afab30562..000000000 --- a/.drive/projects/stage-as-branch/spec.md +++ /dev/null @@ -1,149 +0,0 @@ -# Slice Spec — Deploy an app to a named stage as an isolated environment - -## At a glance - -`prisma-app deploy --stage ` provisions the topology into an isolated **Branch** -of the app's single **Project** — its own compute, database, config, and Alchemy state — -so an app can have production plus staging plus per-PR previews. Implements -[ADR-0024](../../../docs/design/90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md). - -## Pinned decisions — no implementer latitude - -Every design/architecture choice is fixed here. An implementer resolves *how to code* -these, never *what they are*. - -1. **Two-phase deploy.** The CLI ensures containers (Project, Branch) via the - Management API **before** running Alchemy; Alchemy then provisions resources - *within* them. Neither Project nor Branch is an Alchemy resource. - -2. **Id threading = environment variables.** The CLI, after resolving, sets - `PRISMA_PROJECT_ID` and (named stages only) `PRISMA_BRANCH_ID` on the `alchemy` child - process — **both `deploy` and `destroy`** (`run-alchemy.ts`), because `alchemy destroy` - re-imports and re-evaluates the same stack, so its target reconstruction needs the same - ids. The `@prisma/app-cloud` target (`control.ts`) reads them from `process.env` — like - the existing `PRISMA_WORKSPACE_ID` — and threads them through lowering. **Read at - lowering time, not construction:** `prismaCloud()` is constructed once when the CLI loads - `prisma-app.config.ts` in the *parent* (before `ensureContainers` has computed the ids), - and again in the *child* (where the ids are set). So `resolveOptions` reads - `PRISMA_PROJECT_ID`/`PRISMA_BRANCH_ID` **without requiring them** (both may be undefined at - parent construction); the *required* check for `projectId` lives in `application.provision`, - which runs only in the child. **No `PrismaCloudOptions` field is added** (the config-file - Project-id override is deferred, per scope); **not** codegen into the stack file. - -3. **Project resolver.** List *live* Projects in the workspace whose name matches the - app name (root system name, or `--name`), oldest-first; **adopt the oldest**; if none, - **create** one. No ownership marker, no `--project` override in this slice (both - deferred). The app name comes from the root `system("", …)`. - -4. **Stage → Branch.** - - **No `--stage` (the default) = production = project level.** No Branch is created; - resources and config are written with **no `branchId`**, exactly as today. Zero - change to current production behavior. - - **`--stage X` (named) = a Branch.** `gitName = X`, which **must pass git - `check-ref-format`**; if invalid, the CLI **fails with a clear error** — no silent - normalization. The Branch is **created-if-absent**: observe first via - `GET /v1/projects/:id/branches?gitName=X` (server-side exact match, ≤1 row), create - via `POST` only when absent, and tolerate a racing `409` by re-observing and adopting - the winner. The branches API has **no** `ifExists`/server-side idempotency field - (verified against `@prisma/management-api-sdk@1.47.0`: the POST body accepts only - `gitName` + `isDefault`); a `409` on duplicate `gitName` is the only signal, so - idempotency is client-side. Its **role is PDP's positional default** (first Branch = - production, later = preview; server-owned, unsettable) — cosmetic for us, does not - affect our provisioning, deferred. - -5. **Provisioning asymmetry (mechanical, no role lookup).** - - Default stage (`branchId` undefined): `Database`, `ComputeService`, `EnvironmentVariable` - written with **no `branchId`**, config `class: production` — exactly as today. - - Named stage (`branchId` set): the `Database` and `ComputeService` are **assigned to the - Branch** (see decision 6 for the mechanism); every `EnvironmentVariable` is written with - **`branchId`** and config `class: preview`. - - The rule is exactly: **`branchId` present ⟺ named stage ⟺ `class: preview`.** The - target computes `class = branchId ? 'preview' : 'production'`; it never reads a Branch - `role`. (Platform-derived `class` is the deferred end-state, ADR-0024.) - -6. **`branchId` on providers — mechanism differs by provider's create body.** Verified - against `@prisma/management-api-sdk@1.47.0`: the `POST /v1/projects/:id/databases` create body - **does NOT accept `branchId`** (only `name`/`region`/`isDefault`/`source`), so a database must - be created project-scoped and then **attached to a Branch by `PATCH /v1/databases/:id` with - `{ branchId }`**. The `.../compute-services` create body *does* accept `branchId` - (`index.d.ts:8628`), so `ComputeService` puts `branchId` **directly in the create body** and - issues **no PATCH** — a project-scoped create-then-PATCH is wrong for compute: compute-service - names are unique **per Branch**, so a project-scoped `POST` lands on the default (`main`) - Branch first, and if a same-named service already exists there (e.g. production), the create - itself is rejected (`compute_service:already_exists`) before the PATCH ever runs. This was - found live on `prisma-app deploy --stage staging` (see design-decisions.md #3). So the two - providers diverge: `Database` — create project-scoped, then `PATCH /v1/databases/:id` with - `{ branchId }` when set (idempotent, self-healing on every reconcile). `ComputeService` — pass - `branchId` in the create body when set; no PATCH at all. `EnvironmentVariable` differs again: - its create body accepts `branchId` + `class` directly (a preview-branch override when - `branchId` is supplied), so it carries both in the create and needs no PATCH. (`Connection` and - `Deployment` are **not** branch members — they inherit the Branch through their parent database - / compute-service — so they take no `branchId`.) - -7. **`application.provision` references, never mints.** It stops calling `Prisma.Project(...)`; - it reads `process.env['PRISMA_PROJECT_ID']` (**required here** — fail clearly if absent) and - emits it as its `projectId` output, which the postgres/compute lowerings read exactly as they - read the minted id today. Project creation is the CLI's job (decision 3). The poison - `DATABASE_URL` vars it writes follow decision 5 (`branchId` + `class: preview` on a named - stage). - -8. **No migrations, no schema.** The framework runs no migrations today and this slice - adds none. A named stage's Postgres is created empty; the `storefront-auth` verify - (`await sql\`select 1\``) needs only a reachable DB, which an empty Postgres satisfies. - Apps that need per-environment schema are out of scope (migrations are a separate, - unbuilt capability). - -9. **State is inherited.** Alchemy's `--stage` already segregates deploy state and - physical names per environment; the store keys by `(stack, stage)`. No state work in - this slice. - -10. **Destroy is explicit — no default target — resolves find-only; teardown removes the - named-stage Branch.** - - **Destroy never defaults to production.** `deploy` may default (no `--stage` = - production); `destroy` must **not** — a bare `destroy ` with no target is a - **hard `CliError`** ("destroy requires an explicit target: `--stage ` for a - branch environment, or `--production` for the production environment"). This prevents - an omitted/typo'd stage from silently tearing down production. - - **Target selection.** `destroy --stage X` tears down branch `X`. `destroy - --production` (a boolean flag) tears down the project-level production environment (no - branch). `--stage` + `--production` together → `CliError` (mutually exclusive). - `--production` is **destroy-only**; passed to `deploy` it is a `CliError` (deploy - already targets production by default). Internally `--production` is just - `stage = undefined` on the find-only path — no new resolver behavior. - - **Find-only.** ensure-containers is **find-only** for destroy: it must never create - anything. `resolveContainer` gains `ensure: boolean` — `deploy` passes `true` - (create-if-absent, decisions 3–4 unchanged), `destroy` passes `false`. On `ensure: - false`, an absent Project (or, for a named stage, an absent Branch) makes the CLI - **fail with a clear "nothing deployed for ``[`/`]"** message — it does - **not** fall back to production. The resolved ids are injected on the `destroy` child - exactly as for deploy (decision 2). - After `alchemy destroy` removes the Branch's members (compute, database, config), the - CLI **soft-deletes the named-stage Branch** via `DELETE /v1/branches/:branchId` — the - members must be gone first (the API refuses to delete a Branch that is the default/ - production Branch or still has live members). The production (default-stage) Branch is - never deleted. **D2 wires the find-only resolution + id injection for both commands; - the branch soft-delete is implemented and proven in D4.** - -## Scope - -**In:** decisions 1–9. **Out (deferred):** explicit `prisma-app create`; explicit branch -`role` control; platform-derived config `class`; migrations / per-branch schema; -ownership marker + `--project` override; copy-on-write preview data; git-triggered -previews; the platform state-API. - -## Slice-DoD - -- `prisma-app deploy` (default) — behavior byte-for-byte unchanged. -- `prisma-app deploy --stage staging` — stands up an isolated staging environment (own - Branch, compute, empty Postgres, `branchId`+preview config) alongside production; - re-deploy is a no-op; **proven live** on `examples/storefront-auth` (staging ingress → - `auth.verify()` returns `{ ok: true }`); `destroy --stage staging` removes the Branch - and its resources without touching production. -- Requires `.env` (`PRISMA_SERVICE_TOKEN` + `PRISMA_WORKSPACE_ID`) copied into the - worktree root for D4. - -## References - -[ADR-0024](../../../docs/design/90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md) -· [ADR-0023](../../../docs/design/90-decisions/ADR-0023-a-prisma-app-is-one-project-a-stage-is-a-branch.md) -· Plan: [`./plan.md`](./plan.md) diff --git a/docs/design/10-domains/deploy-cli.md b/docs/design/10-domains/deploy-cli.md index b25e39fc6..f0c34a5aa 100644 --- a/docs/design/10-domains/deploy-cli.md +++ b/docs/design/10-domains/deploy-cli.md @@ -110,6 +110,11 @@ targets **production**, at the Project level; `--stage ` targets a - **Id threading.** The resolved `projectId` (and, for a named stage, `branchId`) are set as `PRISMA_PROJECT_ID`/`PRISMA_BRANCH_ID` on the `alchemy` child process, for both `deploy` and `destroy`. +- **Targets read the ids at lowering time, not construction.** An extension is + constructed twice: once when the CLI loads `prisma-app.config.ts` in the + parent — *before* the ids exist — and again in the alchemy child, where they + are set. So an extension's constructor must tolerate the ids being absent; + only its lowering hooks (which run in the child) may require them. - **Destroy is explicit.** `prisma-app destroy` requires `--stage ` or `--production`; a bare `destroy` is an error, so an omitted or mistyped stage can never silently tear down production. `destroy` resolves diff --git a/gotchas.md b/gotchas.md index 882287878..163b8f32a 100644 --- a/gotchas.md +++ b/gotchas.md @@ -24,6 +24,8 @@ The capture workflow is the Ignite `product-record-gotcha` skill. - [Next.js on Compute ignores runtime env vars unless the route is force-dynamic](#nextjs-on-compute-ignores-runtime-env-vars-unless-the-route-is-force-dynamic) - [Connection create/read response buries the real Postgres DSN under endpoints.*; `url` is an API self-link](#connection-createread-response-buries-the-real-postgres-dsn-under-endpoints-url-is-an-api-self-link) - [Compute's bun auto-installs at runtime — masks incomplete artifacts and cross-platform native binaries as an ENOSPC crash loop](#computes-bun-auto-installs-at-runtime--masks-incomplete-artifacts-and-cross-platform-native-binaries-as-an-enospc-crash-loop) +- [Branch create has no idempotency — a duplicate gitName 409s, with no create-or-return](#branch-create-has-no-idempotency--a-duplicate-gitname-409s-with-no-create-or-return) +- [A project-scoped compute-service create lands on the default branch — and collides with production](#a-project-scoped-compute-service-create-lands-on-the-default-branch--and-collides-with-production) --- @@ -247,3 +249,45 @@ process.on("unhandledRejection", (e) => console.error(e)); - Upstream: [PRO-213](https://linear.app/prisma-company/issue/PRO-213/compute-runs-bun-with-runtime-auto-install-on-masks-incomplete) - Fix: [`packages/app-nextjs/src/assemble.ts`](packages/app-nextjs/src/assemble.ts), [`examples/storefront-auth/systems/storefront/next.config.ts`](examples/storefront-auth/systems/storefront/next.config.ts) - Related: PRO-201 (Next standalone packaging), FT-5219 (Bun.SQL scale-to-zero) + +--- + +## Branch create has no idempotency — a duplicate gitName 409s, with no create-or-return + +**Filed upstream:** [PRO-214](https://linear.app/prisma-company/issue/PRO-214/management-api-branch-create-has-no-idempotency-ifexists-409-on) — _"Management API: branch create has no idempotency (`ifExists`) — 409 on duplicate gitName"_ +**Product:** Prisma Postgres / Compute (Management API, branches) +**Version:** `@prisma/management-api-sdk` 1.47.0 +**First hit:** the deploy CLI's ensure-containers step (`resolveBranch`), building stage-as-branch +**Cost:** low — caught at implementation; the client-side dance is boilerplate every caller repeats. + +**Symptom.** `POST /v1/projects/{projectId}/branches` with a `gitName` that already exists returns `409`, full stop. Any "ensure this branch exists" step that runs on every deploy cannot just create. + +**Cause.** The create body accepts only `gitName` + `isDefault` (`additionalProperties: false`) — there is no `ifExists`/upsert option, so idempotency must be client-side. + +**Workaround.** Observe first (`GET …/branches?gitName=X` — server-side exact match, ≤1 row), `POST` only when absent, and on a racing `409` re-read and adopt the winner instead of failing. + +**References.** + +- Upstream: [PRO-214](https://linear.app/prisma-company/issue/PRO-214/management-api-branch-create-has-no-idempotency-ifexists-409-on) +- Fix: [`packages/alchemy/src/container.ts`](packages/alchemy/src/container.ts) (`resolveBranch`) + +--- + +## A project-scoped compute-service create lands on the default branch — and collides with production + +**Filed upstream:** [PRO-215](https://linear.app/prisma-company/issue/PRO-215/management-api-project-scoped-compute-service-create-collides-with) — _"Management API: project-scoped compute-service create collides with production on `main`; branchId-on-create differs from databases"_ +**Product:** Prisma Compute (Management API) +**Version:** `@prisma/management-api-sdk` 1.47.0 +**First hit:** `prisma-app deploy --stage staging` on `examples/storefront-auth` — the stage-as-branch live proof +**Cost:** ~1 hour — one failed live deploy, diagnosis, and a provider rework. + +**Symptom.** Deploying a same-named compute service into a preview Branch fails outright: `compute_service:already_exists: An app named "auth" already exists on branch "main"`. + +**Cause.** `POST /v1/projects/{projectId}/compute-services` with no `branchId` lands the service on the project's default (`main`) Branch, and compute-service names are unique **per Branch** — so the create collides with the production service before any later branch-attach can run. Databases are the mirror image: their create body has **no** `branchId` at all (attach is a `PATCH` after create), so the two sibling resource types need opposite mechanisms and the naive uniform approach hard-fails only for compute. + +**Workaround.** For compute services, pass `branchId` in the create body (create directly on the target Branch; no PATCH). For databases, create project-scoped, then `PATCH /v1/databases/{id}` with `{ branchId }`. + +**References.** + +- Upstream: [PRO-215](https://linear.app/prisma-company/issue/PRO-215/management-api-project-scoped-compute-service-create-collides-with) +- Fix: [`packages/alchemy/src/compute/ComputeService.ts`](packages/alchemy/src/compute/ComputeService.ts), [`packages/alchemy/src/postgres/Database.ts`](packages/alchemy/src/postgres/Database.ts)