Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .drive/projects/build-reporting/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Nothing is blocked. What remains before the definition of done is met is one rea

## Follow-up design, 2026-08-13

The topology design is settled; the canonical spec lives in pdp-control-plane at `projects/branch-topology/spec.md` (see [topology-design.md](topology-design.md) for the pointer). Composer's follow-up slices, from its plan: (1) keep the authored graph — boundary ports and pre-dereference edges — in core's `Graph` (`load-module.ts` + `graph-types.ts`, additive); (2) the pre-apply topology submission through the `ExtensionDescriptor` seam once the endpoint exists, best-effort like all reporting; (3) stamp `logicalId` (the node's full address, already on `LowerContext.address`) on every typed row the providers create — Database, Bucket, Service — once the platform accepts the column; (4) retire `--report` once the Action reads the platform; (5) reject `$out` as a user-declared port name.
The topology design is settled; the canonical spec lives in pdp-control-plane at `projects/branch-topology/spec.md` (see [topology-design.md](topology-design.md) for the pointer). Composer's follow-up slices, from its plan: (1) keep the authored graph — boundary ports and pre-dereference edges — in core's `Graph` (`load-module.ts` + `graph-types.ts`, additive); (2) the pre-apply topology submission through the `ExtensionDescriptor` seam once the endpoint exists, best-effort like all reporting; (3) stamp `logicalId` (the node's full address, already on `LowerContext.address`) on every typed row the providers create — Database, Bucket, Service — the column has shipped; the Alchemy upgrade that adds the prop must pass `logicalId: address` in the same change (the rule is recorded as [ADR-0051](../../../docs/design/90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md); the rows need the upstream alchemy `logicalId` prop, see [alchemy-lowering.md § Platform identity](../../../docs/design/05-prisma-cloud/alchemy-lowering.md#platform-identity-logicalid)); (4) retire `--report` once the Action reads the platform; (5) reject `$out` as a user-declared port name.

**Identity adoption (2026-08-13, operator direction; shipped).** Composer treats the module name as the project's `logicalId` and every node address as that node's `logicalId` — the same identity the topology submits. Project-level resolution by that identity landed via [PR #230](https://github.com/prisma/composer/pull/230), and the field shipped platform-side as `logicalId` (the slug→logicalId rename happened before the window closed); composer main sends and matches `logicalId` today. The broader domain model — Build Run, Versions, branch-scoped resources, the topology content hash — is pdp ADR-012 ([pdp#4902](https://github.com/prisma/pdp-control-plane/pull/4902)); when the Build Run widening ships, composer's resource reporting gains per-link outcomes and the topology submission records its content hash on the run. The address form is settled and verified against core (`load-module.ts`): the root scope's children get bare, unprefixed addresses (`auth.api`, not `shop.auth.api`); the root node's address is its own name, which is also the Project's `logicalId`. Stamp and submit addresses exactly as the graph declares them.

Expand Down Expand Up @@ -112,3 +112,11 @@ Recorded in `spec.md` as D4, D5, D6 and D7, all taken by the orchestrator and al
## Not verifiable yet — superseded

This section predates the API shipping. The stack merged, the API is in production, and the live deploy was observed (see the status header): the definition of done is met.

## Open follow-ups from ADR-0051 (2026-10-09)

- With `prisma deploy --name`, the Project's `logicalId` is the `--name` value, but the topology root node is submitted under the module's own name: `pipeline.ts` calls `Load(entryModule.root)` without the override, while the deploy child uses `Load(root, { id: opts.name })`. Fix: load the reported graph with the same id.
- Slice 3 itself: upgrade alchemy to a release containing alchemy-run/alchemy#1849 and pass `logicalId: id` on `Prisma.App`, `Prisma.Database` and `Prisma.Bucket`, in one change. Supersedes prisma/composer#344.
- Projects created before they carried a `logicalId` are found by display name and never backfilled. The fallback in `resolveProject` (`container.ts`) also matches a Project that already has a different `logicalId`; it should only consider Projects with no `logicalId`.
- After slice 3: document how to recover when a deploy that lost its state hits a duplicate-`logicalId` 409 (Composer never adopts the existing row), and check that the local-target providers accept and ignore the new `logicalId` prop.
- The pdp-control-plane branch-topology spec's worked example uses `catalog-db` as a node `logicalId`. Composer can't produce that address (provision IDs are letters and digits only), and it reads like an Alchemy resource ID. Ask pdp to change the example node's `logicalId` to `catalog`.
4 changes: 4 additions & 0 deletions docs/design/01-principles/architectural-principles.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ Control-plane code (inferring, emitting, provisioning) and execution-plane code
(running your app) live behind separate imports, so build-time machinery never
lands in your application bundle. You ship only what runs.

## A node's address is its identity, everywhere

A node's address (`auth.api`, the path of provision IDs from the root) is its one identity. A provision ID is the node's name unless `provision()` sets one, so the names an author writes are, by default, identity. Everything that needs to say "this node" is derived from the address: config keys and the boot address inside the deploy, and, verbatim, the node's identity in whatever topology and platform rows a target records. Internal names, such as the deploy engine's resource IDs, display labels and IDs a platform generates, are never used as identity. Changing an address makes a different entity; changing a label changes nothing. See [ADR-0051](../90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md) for how Prisma Cloud records it.

## The framework has no knowledge of specific deployment targets

The core deals only in the abstract model — Modules, inputs, outputs, resources — and
Expand Down
4 changes: 4 additions & 0 deletions docs/design/03-domain-model/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ whole graph be recreated in a fresh environment and reproduced in the local
emulator (see the [goals](../00-purpose/goals.md)). Anything without a lifecycle is
Configuration, not a node.

### Address and logical ID

A node's **address** is its identity: the path of provision IDs from the root, assigned by Load (`auth.api`). A provision ID is the node's name unless `provision()` sets an `id`, so a node's name is, by default, part of its identity. The root's direct children have bare addresses, and the root's address is the application's name. Below the root, addresses contain only letters, digits and dots. On Prisma Cloud the address is the node's **logical ID** (`logicalId`): Composer writes it, byte for byte, on the node's topology entry and on the node's one platform row (Project, App, Database or Bucket). The same node on two branches has the same logical ID. A **display name** is a label only and never identity. An **Alchemy resource ID** (`catalog-db`, `web-svc`) is Composer's internal naming for the resources a node lowers to, and is never an identity; Alchemy's own docs call it a "logical ID", which is a different value from the platform's `logicalId`. Changing a node's address, including by changing its name, makes it a different entity; it is not a rename. See [ADR-0051](../90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md).

## Connections

A **connection** is an edge that wires one node's **Output** to another node's
Expand Down
42 changes: 29 additions & 13 deletions docs/design/05-prisma-cloud/alchemy-lowering.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ has no support yet (buckets) or no Management API exists behind them.
| `Prisma.EnvironmentVariable` | ConfigVariable | project, class, key, value (Redacted), branchId? | environmentVariableId | production-class with no `branchId` on the default stage; preview-class with `branchId` on a named stage. Values are write-only, so upstream re-applies the desired one on every deploy |
| `Prisma.Deployment` | Deployment (ComputeVersion) + Promotion | app, artifactPath, artifactContentType, portMapping, triggers, start, promote | deploymentId, appEndpointDomain | provider reconcile: create → upload tar.gz → start → poll until running → promote; `appEndpointDomain` read **post-promote** (create-time domain is a placeholder — PRO-200). It is replaced, not updated, when its artifact fingerprint or its `triggers` fingerprint moves |

The props above are the ones Composer passes today. The `logicalId` each node's row must carry is in [§ Platform identity](#platform-identity-logicalid).

What we deliberately do **not** model yet, and where it will bite:
**Promotion** as a standalone resource (the Deployment provider
auto-promotes; rollback is unexpressed), and non-default **Databases** with
Expand All @@ -101,21 +103,34 @@ only as a container id carried in providers' `branchId` props; it is never an
Alchemy resource itself, since its lifecycle lives outside Alchemy
(ADR-0024).

## Platform identity: `logicalId`

A node that has a platform row has exactly one row that represents it, and that row's `logicalId` is the node's address, byte for byte ([ADR-0051](../90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md)). The application topology submitted on each deploy uses the same string for the node, so the platform can match the two.

| Node | Row that carries the `logicalId` | Written by |
| --- | --- | --- |
| the root | Project | container resolution, before Alchemy runs: `POST /v1/projects` with `logicalId`, and later deploys find the Project by it |
| compute service | App (the platform's Service table) | ``Prisma.App(`${address}-svc`, { …, logicalId: address })`` |
| postgres resource | Database | ``Prisma.Database(`${address}-db`, { …, logicalId: address })`` |
| bucket | Bucket | ``Prisma.Bucket(`${address}-bucket`, { …, logicalId: address })`` |
| module | none | — |

Rules:

- The value is the address, never the Alchemy resource ID. Upstream's Prisma resources default `logicalId` to their fully qualified resource ID (`catalog-db` for Composer), so the lowering always passes it explicitly.
- For App, Database and Bucket, the resource that creates the row writes its `logicalId`, as a prop. No separate resource or later API call writes it.
- Other platform rows a node lowers to carry no `logicalId`: `Prisma.Connection`, `Prisma.Deployment`, `Prisma.EnvironmentVariable`, `Prisma.BucketAccessKey`. Composer's local resources (`ServiceKey`, `PgWarm`, `OrmMigration`, `GeneratedParam`, `S3Credentials`) create no platform row.
- Branchless local dev creates no platform rows and writes none.

Status:

- The Project carries its `logicalId` when Composer created it. Projects created before that are found by display name and have none.
- App, Database and Bucket need the `logicalId` prop that upstream alchemy added in [alchemy-run/alchemy#1849](https://github.com/alchemy-run/alchemy/pull/1849). Until Composer upgrades to a release that includes it, those rows have no `logicalId` and match no topology node. The upgrade must pass `logicalId: address` in the same change: an upgrade alone would write the Alchemy resource ID (`catalog-db`) onto every existing row.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Minor · consistency — Bucket logicalId writer attributed to upstream alchemy#1849, contradicting the cited ADR-0048

The status bullet says the App, Database and Bucket rows get their logicalId prop from "the upstream Alchemy resource that creates the row", citing ADR-0048 (ADR-0051 states the same at line 49). But ADR-0048 still says Composer "defines its own resources only where the upstream provider has no support yet (buckets, whose routes upstream deferred)". Current code uses upstream Prisma.Bucket/Prisma.BucketProvider() (providers.ts, descriptors/bucket.ts), so this ADR matches the code — but a reader following the citation lands in a document that states the opposite, and unlike ADR-0006/ADR-0024, ADR-0048 got no amendment note for a decision this ADR relies on.

Recommended fix

Add an amendment note to ADR-0048 (and its README index entry) recording that the bucket family now composes upstream's Prisma.Bucket provider, mirroring the ADR-0006/ADR-0024 amendment notes this ADR added — or soften the citation here and in ADR-0051.

- With `prisma deploy --name`, the Project's `logicalId` is the `--name` value, but the topology's root node is still submitted under the module's own name, so the two don't match. The CLI loads the graph it reports without the override. This is a defect.

## Stages and container resolution

`@internal/lowering` also hosts the **container-resolution client**
(`resolveContainer` / `deleteBranch`) the deploy CLI runs *before* the
generated stack, not through an Alchemy resource: `resolveContainer`
finds-or-creates the app's Project (oldest name match adopted) and, for a
named stage, its Branch (found by `gitName`, created if absent); `ensure:
false` makes it find-only, for `destroy`. It reuses the same Management API
client and the same adopt-oldest / tolerate-a-racing-409 idiom the state
store's own bootstrap uses
([ADR-0034](../90-decisions/ADR-0034-deploy-state-lives-in-the-stage-branch.md))
— the two resolve different things (deploy containers vs. the stage's state
database) through the same client and idiom. Once `destroy` has removed a
stage's members, the CLI removes the stage's state database
(ownership-verified) and `deleteBranch` then soft-deletes its Branch.
`@internal/lowering` also hosts the **container-resolution client** (`resolveContainer` / `deleteBranch`) the deploy CLI runs *before* the generated stack, not through an Alchemy resource: `resolveContainer` finds-or-creates the app's Project (by `logicalId`, the app name; when no Project has it, by display name, oldest first) and, for a named stage, its Branch (found by `gitName`, created if absent); `ensure: false` makes it find-only, for `destroy`. It reuses the same Management API client and the same adopt-oldest / tolerate-a-racing-409 idiom the state store's own bootstrap uses ([ADR-0034](../90-decisions/ADR-0034-deploy-state-lives-in-the-stage-branch.md)) — the two resolve different things (deploy containers vs. the stage's state database) through the same client and idiom. Once `destroy` has removed a stage's members, the CLI removes the stage's state database (ownership-verified) and `deleteBranch` then soft-deletes its Branch.

Deploy state keeps its existing shape — keyed per Alchemy `--stage`
(ADR-0034) — unchanged by this: under stage-as-branch, **the Project is the
Expand Down Expand Up @@ -233,3 +248,4 @@ author and no app author ever hand-wires them.
- [ADR-0023](../90-decisions/ADR-0023-a-prisma-app-is-one-project-a-stage-is-a-branch.md)
/ [ADR-0024](../90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md)
— the decisions this section documents.
- [ADR-0051](../90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md) — a node's address is its `logicalId` on the platform.
12 changes: 12 additions & 0 deletions docs/design/05-prisma-cloud/pdp-data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ Edge semantics, with the properties that matter to us:
Foundry version stores `envVars`, `portMapping`, `previewDomain`, `status` —
PDP's `GET /versions/:id` reads them back from Foundry.

## Identity: `id`, `logicalId`, `displayName`

Project, App (the platform's Service table), Database and Bucket rows each carry three identifiers:

| Identifier | Set by | Identifies | Unique within |
| --- | --- | --- | --- |
| `id` | the platform | one physical row | everywhere |
| `logicalId` | the configuration | the declared entity, on every branch | its Branch (a Project: its workspace) |
| `displayName` | Composer or the platform; Console can change it | nothing; it is a label | nothing |

Each Branch can also store an **application topology**: the declared graph of nodes, ports and edges, every node named by `logicalId` and none by `id` (`PUT /v1/projects/{projectId}/branches/{branchId}/application-topology`). The platform matches topology nodes to rows by string equality on `logicalId`. Composer writes a node's address into both ([ADR-0051](../90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md)). The platform's design is the pdp-control-plane [branch topology spec](https://github.com/prisma/pdp-control-plane/blob/main/projects/branch-topology/spec.md).

## The config lifecycle — what is resolved when

This is the timing model Prisma Composer's graph must respect:
Expand Down
Loading
Loading