From cc63930cffafe643e40c9cda6abf56f54bb253ec Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 9 Oct 2026 11:35:19 +0200 Subject: [PATCH 1/3] docs(design): a node's address is its logical ID on the platform (ADR-0051) Record that Composer writes each node's address, byte for byte, as the logicalId of the node's platform row and of its topology node. Add the rule to the architectural principles, glossary, core model, PDP data model and Alchemy lowering docs. Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- .drive/projects/build-reporting/plan.md | 2 +- .../01-principles/architectural-principles.md | 4 + docs/design/03-domain-model/glossary.md | 4 + .../05-prisma-cloud/alchemy-lowering.md | 22 ++++++ docs/design/05-prisma-cloud/pdp-data-model.md | 12 +++ docs/design/10-domains/core-model.md | 6 +- ...dress-is-its-logical-id-on-the-platform.md | 74 +++++++++++++++++++ docs/design/90-decisions/README.md | 1 + 8 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 docs/design/90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md diff --git a/.drive/projects/build-reporting/plan.md b/.drive/projects/build-reporting/plan.md index 1caf8b588..2ce111b28 100644 --- a/.drive/projects/build-reporting/plan.md +++ b/.drive/projects/build-reporting/plan.md @@ -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 — once the platform accepts the column (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. diff --git a/docs/design/01-principles/architectural-principles.md b/docs/design/01-principles/architectural-principles.md index 2a9da5513..85339a020 100644 --- a/docs/design/01-principles/architectural-principles.md +++ b/docs/design/01-principles/architectural-principles.md @@ -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. Everything that needs to say "this node" is derived from it: config keys and the boot address inside the deploy, and, verbatim, the topology the platform stores and the platform rows a target creates for the node. On Prisma Cloud it is the `logicalId` of the node's row, which is how the platform matches a topology node to its row and one node across branches. Internal names, such as the deploy engine's resource IDs (`catalog-db`), display names and platform-generated IDs, are never used as identity. Changing an address makes a different entity; changing a display name changes nothing. See [ADR-0051](../90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md). + ## The framework has no knowledge of specific deployment targets The core deals only in the abstract model — Modules, inputs, outputs, resources — and diff --git a/docs/design/03-domain-model/glossary.md b/docs/design/03-domain-model/glossary.md index 1db88e32c..0053bc411 100644 --- a/docs/design/03-domain-model/glossary.md +++ b/docs/design/03-domain-model/glossary.md @@ -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`). The root's direct children have bare addresses, and the root's address is the application's name. 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 and different platform `id`s. A **display name** is a label only; renaming it changes nothing. 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. 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 diff --git a/docs/design/05-prisma-cloud/alchemy-lowering.md b/docs/design/05-prisma-cloud/alchemy-lowering.md index 6804e3218..77460cb07 100644 --- a/docs/design/05-prisma-cloud/alchemy-lowering.md +++ b/docs/design/05-prisma-cloud/alchemy-lowering.md @@ -101,6 +101,27 @@ 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` + +Each node has exactly one platform 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`; the Project is then found by it) | +| compute service | App | `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 own resource ID (`catalog-db`), so the lowering always passes it explicitly. +- The resource that creates the row writes its `logicalId`, as a prop. No separate resource or later API call writes it. +- Supporting resources (`Prisma.Connection`, `Prisma.Deployment`, `Prisma.EnvironmentVariable`, `ServiceKey`, `PgWarm`) carry no `logicalId`. They are not topology nodes. +- Branchless local dev creates no platform rows and writes none. + +Status: the Project carries its `logicalId` today. 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. + ## Stages and container resolution `@internal/lowering` also hosts the **container-resolution client** @@ -233,3 +254,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. diff --git a/docs/design/05-prisma-cloud/pdp-data-model.md b/docs/design/05-prisma-cloud/pdp-data-model.md index d83c07620..03e3bc659 100644 --- a/docs/design/05-prisma-cloud/pdp-data-model.md +++ b/docs/design/05-prisma-cloud/pdp-data-model.md @@ -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, 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` | the user | 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: diff --git a/docs/design/10-domains/core-model.md b/docs/design/10-domains/core-model.md index 8bcfd7a1e..554385666 100644 --- a/docs/design/10-domains/core-model.md +++ b/docs/design/10-domains/core-model.md @@ -770,9 +770,9 @@ the printed bootstrap — so unchanged services hash identically and noop, once app's build is itself deterministic (the Next standalone case is a named follow-up). Because the same Load walk feeds both `serialize`'s env keys and the bootstrap's address (and the pack derives config keys from that address on both -sides), the config writer and the boot-time reader cannot drift. An address changes -only when the graph position changes (e.g. a rename), which correctly cascades: new -keys, new bootstrap, new version. +sides), the config writer and the boot-time reader cannot drift. An address changes only when the graph position changes (a changed provision ID, or a move into another module), which correctly cascades: new keys, new bootstrap, new version. + +The address is also the node's identity on the platform. Composer writes it as the `logicalId` of the node's platform row and of its node in the application topology, so a changed address is a different entity there too ([ADR-0051](../90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md)). Notes: diff --git a/docs/design/90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md b/docs/design/90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md new file mode 100644 index 000000000..ea46228eb --- /dev/null +++ b/docs/design/90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md @@ -0,0 +1,74 @@ +# ADR-0051: A node's address is its logical ID on the platform + +## Decision + +Every node's address is its identity on Prisma Cloud. Composer writes the address, byte for byte, as the `logicalId` of the one platform row that represents the node, and submits the same string as the node's `logicalId` in the branch's application topology. Nothing else is ever written to `logicalId`. + +```ts +export default module('shop', ({ provision }) => { + const catalog = provision(rawPostgres({ name: 'catalog' })); + const auth = provision(authModule, { deps: { db: catalog } }); // module `auth`, containing compute service `api` + provision(webService, { deps: { db: catalog, auth: auth.rpc } }); // compute service `web` +}); +``` + +``` +node address topology node platform row +──────────── ──────────────────── ───────────────────────────────────────── +shop logicalId "shop" Project logicalId "shop" +catalog logicalId "catalog" Database logicalId "catalog" id db_cm3x… +auth logicalId "auth" (a module has no row) +auth.api logicalId "auth.api" App logicalId "auth.api" id app_7f2… +web logicalId "web" App logicalId "web" id app_k91… +``` + +The same configuration deployed to the stage `pr-123` produces different `id`s and the same `logicalId`s. That is how the platform knows `web` on `pr-123` is the same service as `web` on `main`. + +## Reasoning + +Every Prisma Cloud entity has three identifiers, each with one job: + +| 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` | the user | nothing; it is a label | nothing | + +Composer already has an identity for every node: its **address**, the path of provision IDs from the root, assigned by Load (`auth.api`). The root's direct children have bare addresses, and the root's address is the application's name (ADR-0006). Addresses are unique within a graph, they are derived from code, and git carries them between branches with the rest of the configuration. That is exactly what `logicalId` is for, so the address is the value. The platform's side of this design is the pdp-control-plane [branch topology spec](https://github.com/prisma/pdp-control-plane/blob/main/projects/branch-topology/spec.md) and its ADR-011. + +The platform depends on the two copies being equal. The application topology Composer submits on each deploy names every node by `logicalId` and contains no platform `id`s, because the rows don't exist yet when it is written. Console and CI then join topology nodes to Project, App, Database and Bucket rows by plain string equality on `logicalId`. A node with no matching row was declared but not created. A row with no matching node is no longer declared. Comparing a database on a preview branch with its counterpart on `main` is the same lookup on two branches. If the row's `logicalId` differs from the node's by even one character, every one of these reads silently finds nothing. + +So the value must be the address, not something that resembles it. In particular, it is never: + +- **an Alchemy resource ID.** Alchemy is the deploy engine Composer lowers to. Composer names Alchemy resources after the address plus a role suffix (`catalog-db`, `catalog-conn`, `web-svc`), because one node lowers to several resources. Those names are Composer's internal naming and appear nowhere in the topology. Upstream Alchemy's Prisma resources default `logicalId` to their own resource ID, so Composer always passes `logicalId` explicitly. +- **a display name.** Users rename display names freely in Console, and a rename must change nothing. +- **a platform `id`.** It differs on every branch and doesn't exist until the row does. + +Only the node's own row carries the `logicalId`. A compute service's row is its App, a postgres resource's row is its Database, a bucket's row is its Bucket, and the root's row is the Project. Supporting rows the lowering creates for a node, such as connections, deployments, environment variables and service keys, are not topology nodes and carry none. A module has no row of its own. + +The `logicalId` is written through the upstream Alchemy resource that creates the row, as a prop on that resource (ADR-0048). Composer doesn't write it with a separate resource or a later API call. One resource owns each row, so one resource owns its `logicalId`. + +## Consequences + +- Changing a node's address changes its identity. The node is then a different entity: the platform and the deploy see a new node, and the old one's rows are deleted. Moving a node into another module, or changing its provision ID, does this. Renaming a display name does not. +- The address format is an identity contract. The platform treats `logicalId` as an opaque string and never parses it, so Composer may change how addresses are composed, but any change re-identifies every existing entity. +- Every node kind that lowers to a platform row must pass the node's address as that row's `logicalId`. A new node kind isn't done until it does. +- A `logicalId` already held by another row on the same Branch fails the deploy. Composer never adopts that row. +- Local dev (ADR-0041) creates no platform rows and writes no `logicalId`. + +## Alternatives considered + +- **Use Alchemy's default (its resource ID).** No extra code, but `catalog-db` never equals the topology's `catalog`. Every join would fail, and the topology would have to submit Composer's internal resource naming instead of the declaration. +- **Write `logicalId` with a separate Composer resource after the row is created.** Duplicates what the upstream resource does (ADR-0048), and puts two resources in charge of one field. When they disagree, they fight on every deploy. +- **Put platform `id`s in the topology.** The `id`s don't exist when the topology is written, before the apply. They also differ on every branch, so they can't say "the same service on `main`". +- **Match on display names.** They are mutable and not unique. Matching Projects by display name forked deploy history on rename, which is why Projects got a `logicalId` first. +- **Have Console read Alchemy's deploy state to map nodes to rows.** That state is Composer's private bookkeeping. Its format belongs to the deploy engine, it contains secrets, and the platform deliberately stores it without reading it. + +## Related + +- [ADR-0006](ADR-0006-every-node-is-named.md) — every node is named; the root's name names the application. +- [ADR-0023](ADR-0023-a-prisma-app-is-one-project-a-stage-is-a-branch.md) — a stage is a Branch. +- [ADR-0048](ADR-0048-prisma-cloud-resources-come-from-the-upstream-alchemy-provider.md) — Prisma Cloud resources come from the upstream Alchemy provider. +- [core-model.md § Deployment identity](../10-domains/core-model.md) — the address. +- [alchemy-lowering.md § Platform identity](../05-prisma-cloud/alchemy-lowering.md#platform-identity-logicalid) — which resource carries the `logicalId` for each node kind. +- [glossary.md § Address and logical ID](../03-domain-model/glossary.md#address-and-logical-id). diff --git a/docs/design/90-decisions/README.md b/docs/design/90-decisions/README.md index 60998449d..34a839adb 100644 --- a/docs/design/90-decisions/README.md +++ b/docs/design/90-decisions/README.md @@ -72,3 +72,4 @@ _Earlier drafts (ADR-0001, ADR-0002) were retired as the high-level design settl - [ADR-0048](ADR-0048-prisma-cloud-resources-come-from-the-upstream-alchemy-provider.md) — The six Management-API resource families (project, database, connection, app, deployment, environment variable) are the upstream `alchemy/Prisma` provider's classes, registered in Composer's `PrismaComposer` collection; Composer defines resources only where the upstream provider has no support yet (buckets) or no Management API exists behind them. Compute binds the low-level App/Deployment/EnvironmentVariable trio (the `COMPOSER_*_ORIGIN` self-edge and ADR-0005 rule out composite `Compute`); the env→deployment ordering edge rides the deployment's `app` prop, and a deployment is replaced exactly when its artifact or environment fingerprint changes — unchanged services are reused. Legacy rows in the platform state API (retired type-ids and shapes) migrate on read — the older retired SQL state stores stay destroy-only under ADR-0045 — branch-stage databases take generated physical names, and the platform's seeded `DATABASE_URL` is left system-managed. - [ADR-0049](ADR-0049-composers-configuration-is-the-composer-section-of-prisma-config.md) — Composer's configuration is the `composer` section of `prisma.config.ts`, loaded by the Prisma CLI engine. The validator checks only the fields that identify extension and state descriptors, passes descriptors through as the file's own objects, rejects unknown keys, and takes the nearest declaring file's section whole so the generated stack imports exactly one file. The old `prisma-composer.config.ts` and the `configPath` pointer are refused with `CONFIG.SECTION_MISSING`, `CONFIG.FIELD_RETIRED` and `CONFIG.FILE_RETIRED`, never migrated. The validator returns the section with its declaring file, and the programmatic operations take the same `ComposerConfigSource` (`{ value, file }`) and run the same checks. The Alchemy child evaluates the whole `prisma.config.ts`. The effect pre-flight and `DEPS.EFFECT_VERSION_CONFLICT` are retired: a broken tree fails evaluation of `prisma.config.ts` with the engine's `CLI.CONFIG_UNREADABLE`. *(Amended: the alternatives section lists the rejected ways to keep a Composer-owned effect check.)* - [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md) — Composer has no binary of its own: the `prisma` CLI mounts exactly two Composer commands, `prisma deploy ` and `prisma dev `, mounted at the root as bare verbs on purpose rather than under a noun. `destroy` and `log` are operations on `@prisma/composer/control`, called from a script; a teardown script reads `PRISMA_SERVICE_TOKEN` and `PRISMA_WORKSPACE_ID`. Where teardown and logs belong in the `prisma` command tree is not decided; until it is, agents and users need a script for both. A `destroy` command was rejected: the CLI's destructive verb is `delete`, and no Composer command takes `--production`. Retires the `prisma-composer` binary; a CI lint keeps its name out of user-facing files. +- [ADR-0051](ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md) — A node's address is its identity on Prisma Cloud: Composer writes it, byte for byte, as the `logicalId` of the node's one platform row (Project, App, Database, Bucket) and as the node's `logicalId` in the application topology, so the platform joins topology nodes to rows, and one node across branches, by string equality. The value is never an Alchemy resource ID, a display name or a platform `id`; it is passed as a prop on the upstream resource that creates the row, never written by a separate resource. Changing an address changes the entity's identity; changing a display name changes nothing. From d0312122ae1ef4820201d4c83780af88e1c0555d Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 9 Oct 2026 11:42:40 +0200 Subject: [PATCH 2/3] docs(design): address review findings on ADR-0051 State that a node name is its default provision ID, so changing it changes identity. Amend ADR-0006 and ADR-0024. Scope the single-writer rule, correct the supporting-row lists, keep the principle target-agnostic, and record the current status and open follow-ups. Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- .drive/projects/build-reporting/plan.md | 9 ++++- .../01-principles/architectural-principles.md | 2 +- docs/design/03-domain-model/glossary.md | 2 +- .../05-prisma-cloud/alchemy-lowering.md | 22 +++++++----- docs/design/05-prisma-cloud/pdp-data-model.md | 4 +-- docs/design/10-domains/core-model.md | 26 ++------------ .../ADR-0006-every-node-is-named.md | 2 ++ ...ironment-resolved-to-project-and-branch.md | 2 ++ ...dress-is-its-logical-id-on-the-platform.md | 35 ++++++++++--------- docs/design/90-decisions/README.md | 6 ++-- 10 files changed, 53 insertions(+), 57 deletions(-) diff --git a/.drive/projects/build-reporting/plan.md b/.drive/projects/build-reporting/plan.md index 2ce111b28..6da2d6b92 100644 --- a/.drive/projects/build-reporting/plan.md +++ b/.drive/projects/build-reporting/plan.md @@ -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 (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. +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. @@ -112,3 +112,10 @@ 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 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 rename the example. diff --git a/docs/design/01-principles/architectural-principles.md b/docs/design/01-principles/architectural-principles.md index 85339a020..06a24cee9 100644 --- a/docs/design/01-principles/architectural-principles.md +++ b/docs/design/01-principles/architectural-principles.md @@ -73,7 +73,7 @@ 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. Everything that needs to say "this node" is derived from it: config keys and the boot address inside the deploy, and, verbatim, the topology the platform stores and the platform rows a target creates for the node. On Prisma Cloud it is the `logicalId` of the node's row, which is how the platform matches a topology node to its row and one node across branches. Internal names, such as the deploy engine's resource IDs (`catalog-db`), display names and platform-generated IDs, are never used as identity. Changing an address makes a different entity; changing a display name changes nothing. See [ADR-0051](../90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md). +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 diff --git a/docs/design/03-domain-model/glossary.md b/docs/design/03-domain-model/glossary.md index 0053bc411..6c42cd87f 100644 --- a/docs/design/03-domain-model/glossary.md +++ b/docs/design/03-domain-model/glossary.md @@ -140,7 +140,7 @@ 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`). The root's direct children have bare addresses, and the root's address is the application's name. 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 and different platform `id`s. A **display name** is a label only; renaming it changes nothing. 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. See [ADR-0051](../90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md). +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. 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 diff --git a/docs/design/05-prisma-cloud/alchemy-lowering.md b/docs/design/05-prisma-cloud/alchemy-lowering.md index 77460cb07..5c087d8d1 100644 --- a/docs/design/05-prisma-cloud/alchemy-lowering.md +++ b/docs/design/05-prisma-cloud/alchemy-lowering.md @@ -107,27 +107,31 @@ Each node has exactly one platform row that represents it, and that row's `logic | Node | Row that carries the `logicalId` | Written by | | --- | --- | --- | -| the root | Project | container resolution, before Alchemy runs (`POST /v1/projects` with `logicalId`; the Project is then found by it) | -| compute service | App | `Prisma.App(\`${address}-svc\`, { …, logicalId: address })` | -| postgres resource | Database | `Prisma.Database(\`${address}-db\`, { …, logicalId: address })` | -| bucket | Bucket | `Prisma.Bucket(\`${address}-bucket\`, { …, logicalId: address })` | +| 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 own resource ID (`catalog-db`), so the lowering always passes it explicitly. -- The resource that creates the row writes its `logicalId`, as a prop. No separate resource or later API call writes it. -- Supporting resources (`Prisma.Connection`, `Prisma.Deployment`, `Prisma.EnvironmentVariable`, `ServiceKey`, `PgWarm`) carry no `logicalId`. They are not topology nodes. +- 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` today. 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. +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. +- 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 +finds-or-creates the app's Project (by `logicalId`, the app name; Projects without one 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 diff --git a/docs/design/05-prisma-cloud/pdp-data-model.md b/docs/design/05-prisma-cloud/pdp-data-model.md index 03e3bc659..53e9e9545 100644 --- a/docs/design/05-prisma-cloud/pdp-data-model.md +++ b/docs/design/05-prisma-cloud/pdp-data-model.md @@ -63,13 +63,13 @@ Edge semantics, with the properties that matter to us: ## Identity: `id`, `logicalId`, `displayName` -Project, App, Database and Bucket rows each carry three identifiers: +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` | the user | nothing; it is a label | nothing | +| `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). diff --git a/docs/design/10-domains/core-model.md b/docs/design/10-domains/core-model.md index 554385666..6aa44b06c 100644 --- a/docs/design/10-domains/core-model.md +++ b/docs/design/10-domains/core-model.md @@ -739,17 +739,7 @@ recreated for), tracked as an upstream follow-up. Secrets are platform-sourced and rotate through the platform, not this edge (see the [config/secret glossary](../03-domain-model/glossary.md#configuration--config-and-secrets)). -**Deployment identity — address, bootstrap, and why.** A node's identity is its -**address**: the path of provision ids from the app root, assigned by Load from -graph position — never user-invented, so registry modules with common internal -names (`db`, `service`) cannot collide; the address qualifies them. Identity -cannot travel through the environment: every App in a Project boots a -byte-identical env (ConfigVariables are project+branch-scoped and snapshotted at -version create — see the [PDP data model](../05-prisma-cloud/pdp-data-model.md)), -so any "who am I" variable is one shared key, last write wins. The only -per-service channel is the artifact itself. Hence the bootstrap: the artifact's -entrypoint takes identity as a parameter, and deploy is the caller. The Compute -artifact the pack assembles: +**Deployment identity — address, bootstrap, and why.** A node's identity is its **address**: the path of provision ids from the app root, assigned by Load from graph position. A provision id is the node's name unless `provision()` sets an `id`, and the path qualifies it, so registry modules with common internal names (`db`, `service`) cannot collide. Identity cannot travel through the environment: every App in a Project boots a byte-identical env (ConfigVariables are project+branch-scoped and snapshotted at version create — see the [PDP data model](../05-prisma-cloud/pdp-data-model.md)), so any "who am I" variable is one shared key, last write wins. The only per-service channel is the artifact itself. Hence the bootstrap: the artifact's entrypoint takes identity as a parameter, and deploy is the caller. The Compute artifact the pack assembles: ``` server.js ← the app's OWN built entry (Hono bundle / Next standalone). Calls service.load(). @@ -758,19 +748,7 @@ bootstrap.js ← pack-printed: `import main from "./main.js"; await ma compute.manifest.json ← pack-written envelope; entrypoint = bootstrap.js ``` -The node carries its own runner: the pack's service node has `run(address, boot)` -and `load()` (§ Runtime), so the wrapper already contains the boot loop — the -bootstrap is a two-line sliver that imports `./main.js` and dynamically imports the -app's entry, and the artifact holds a single copy of core. There is **no import -cycle**: the app's entry imports the service module (for `load()`), and the -bootstrap imports the wrapper and dynamically imports the entry — nothing imports -the bootstrap, and the serve code lives in the app's entry, never in the service -module. Every byte is deterministic — the app's built entry, the framework's wrapper, -the printed bootstrap — so unchanged services hash identically and noop, once the -app's build is itself deterministic (the Next standalone case is a named -follow-up). Because the same Load walk feeds both `serialize`'s env keys and the -bootstrap's address (and the pack derives config keys from that address on both -sides), the config writer and the boot-time reader cannot drift. An address changes only when the graph position changes (a changed provision ID, or a move into another module), which correctly cascades: new keys, new bootstrap, new version. +The node carries its own runner: the pack's service node has `run(address, boot)` and `load()` (§ Runtime), so the wrapper already contains the boot loop — the bootstrap is a two-line sliver that imports `./main.js` and dynamically imports the app's entry, and the artifact holds a single copy of core. There is **no import cycle**: the app's entry imports the service module (for `load()`), and the bootstrap imports the wrapper and dynamically imports the entry — nothing imports the bootstrap, and the serve code lives in the app's entry, never in the service module. Every byte is deterministic — the app's built entry, the framework's wrapper, the printed bootstrap — so unchanged services hash identically and noop, once the app's build is itself deterministic (the Next standalone case is a named follow-up). Because the same Load walk feeds both `serialize`'s env keys and the bootstrap's address (and the pack derives config keys from that address on both sides), the config writer and the boot-time reader cannot drift. An address changes only when the graph position changes (a changed provision ID, or a move into another module), which correctly cascades: new keys, new bootstrap, new version. The address is also the node's identity on the platform. Composer writes it as the `logicalId` of the node's platform row and of its node in the application topology, so a changed address is a different entity there too ([ADR-0051](../90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md)). diff --git a/docs/design/90-decisions/ADR-0006-every-node-is-named.md b/docs/design/90-decisions/ADR-0006-every-node-is-named.md index 8512f88cb..c87df872f 100644 --- a/docs/design/90-decisions/ADR-0006-every-node-is-named.md +++ b/docs/design/90-decisions/ADR-0006-every-node-is-named.md @@ -2,6 +2,8 @@ Amended by [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md): the override is `prisma deploy --name`; the `prisma-composer` binary is retired. The naming rules below are unchanged. +Amended by [ADR-0051](ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md): a node's name is its provision ID unless `provision()` sets an `id`, so by default every node's name is part of its address, and the address is its identity on the platform (`logicalId`). Changing any node's name therefore changes its identity, not just the root's; for a database or bucket that deletes its data. Where this ADR says "renaming" a node, it means changing that identity, not changing a display name. + ## Decision Every node — module, service, or resource — carries an explicit, diff --git a/docs/design/90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md b/docs/design/90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md index 3e4a4f5e0..551e8223e 100644 --- a/docs/design/90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md +++ b/docs/design/90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md @@ -2,6 +2,8 @@ Amended by [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md): deploys run as `prisma deploy [--stage ]`. Teardown is the `destroy` operation on `@prisma/composer/control`, whose required `target` is `{ kind: 'stage', stage }` or `{ kind: 'production' }`; there is no destroy command. The `prisma-composer` binary is retired. Stage resolution below is unchanged. +Amended by [ADR-0051](ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md): the CLI finds the Project by its `logicalId`, which is the app name, and creates new Projects with it. Matching by display name, oldest first, remains only as a fallback for Projects created before they carried a `logicalId`. + ## Decision An app is deployed to a named **stage** — an environment. The topology is diff --git a/docs/design/90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md b/docs/design/90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md index ea46228eb..187ac1391 100644 --- a/docs/design/90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md +++ b/docs/design/90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md @@ -22,38 +22,40 @@ auth.api logicalId "auth.api" App logicalId "auth.api" id app_7f2 web logicalId "web" App logicalId "web" id app_k91… ``` -The same configuration deployed to the stage `pr-123` produces different `id`s and the same `logicalId`s. That is how the platform knows `web` on `pr-123` is the same service as `web` on `main`. +Deploying the same configuration to the stage `pr-123` creates new App and Database rows on that stage's Branch, with different `id`s and the same `logicalId`s. That is how the platform knows `web` on `pr-123` is the same service as `web` on `main`. ## Reasoning -Every Prisma Cloud entity has three identifiers, each with one job: +Prisma Cloud's Project, App, Database and Bucket rows each carry three identifiers, each with one job: | 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` | the user | nothing; it is a label | nothing | +| `displayName` | Composer or the platform; Console can change it | nothing; it is a label | nothing | -Composer already has an identity for every node: its **address**, the path of provision IDs from the root, assigned by Load (`auth.api`). The root's direct children have bare addresses, and the root's address is the application's name (ADR-0006). Addresses are unique within a graph, they are derived from code, and git carries them between branches with the rest of the configuration. That is exactly what `logicalId` is for, so the address is the value. The platform's side of this design is the pdp-control-plane [branch topology spec](https://github.com/prisma/pdp-control-plane/blob/main/projects/branch-topology/spec.md) and its ADR-011. +Composer already has an identity for every node: its **address**. The address is the path of provision IDs from the root, assigned by Load (`auth.api`). A node's provision ID is its `name` unless `provision(node, { id })` sets one, so the name an author writes is, by default, part of the node's identity. The root's direct children have bare addresses, and the root's address is the application's name: the root's name, or `--name` for a deploy (ADR-0006). Provision IDs are letters and digits only, so an address contains only letters, digits and dots. Addresses are unique within a graph, they are derived from code, and git carries them between branches with the rest of the configuration. That is exactly what `logicalId` is for, so the address is the value. The platform's side of this design is the pdp-control-plane [branch topology spec](https://github.com/prisma/pdp-control-plane/blob/main/projects/branch-topology/spec.md). -The platform depends on the two copies being equal. The application topology Composer submits on each deploy names every node by `logicalId` and contains no platform `id`s, because the rows don't exist yet when it is written. Console and CI then join topology nodes to Project, App, Database and Bucket rows by plain string equality on `logicalId`. A node with no matching row was declared but not created. A row with no matching node is no longer declared. Comparing a database on a preview branch with its counterpart on `main` is the same lookup on two branches. If the row's `logicalId` differs from the node's by even one character, every one of these reads silently finds nothing. +The platform depends on the two copies being equal. The application topology Composer submits on each deploy names every node by `logicalId` and contains no platform `id`s, because the rows don't exist yet when it is written. Console and CI then join topology nodes to rows by plain string equality on `logicalId`. A node with no matching row was declared but not created. A row whose `logicalId` matches no node is no longer declared. A row with no `logicalId` matches nothing. Comparing a database on a preview branch with its counterpart on `main` is the same lookup on two branches. If the row's `logicalId` differs from the node's by even one character, every one of these reads silently finds nothing. So the value must be the address, not something that resembles it. In particular, it is never: -- **an Alchemy resource ID.** Alchemy is the deploy engine Composer lowers to. Composer names Alchemy resources after the address plus a role suffix (`catalog-db`, `catalog-conn`, `web-svc`), because one node lowers to several resources. Those names are Composer's internal naming and appear nowhere in the topology. Upstream Alchemy's Prisma resources default `logicalId` to their own resource ID, so Composer always passes `logicalId` explicitly. -- **a display name.** Users rename display names freely in Console, and a rename must change nothing. +- **an Alchemy resource ID.** Alchemy is the deploy engine Composer lowers to. Composer names Alchemy resources after the address plus a role suffix (`catalog-db`, `catalog-conn`, `web-svc`), because one node lowers to several resources. Those names are Composer's internal naming and appear nowhere in the topology. A value containing `-` is never an address, so it is always one of these. Alchemy's own documentation calls its resource ID a "logical ID". That is a different value from the platform's `logicalId`, and the shared words must not be read as the same thing. Upstream Alchemy's Prisma resources default `logicalId` to their fully qualified resource ID, which for Composer is the resource ID, so Composer always passes `logicalId` explicitly. +- **a display name.** It is a label. Composer often derives it from the address, so the two can look alike, but it carries no identity. - **a platform `id`.** It differs on every branch and doesn't exist until the row does. -Only the node's own row carries the `logicalId`. A compute service's row is its App, a postgres resource's row is its Database, a bucket's row is its Bucket, and the root's row is the Project. Supporting rows the lowering creates for a node, such as connections, deployments, environment variables and service keys, are not topology nodes and carry none. A module has no row of its own. +Only the node's own row carries the `logicalId`. A compute service's row is its App, a postgres resource's row is its Database, a bucket's row is its Bucket, and the root's row is the Project. Other platform rows the lowering creates for a node, such as connections, deployments, environment variables and bucket access keys, are not topology nodes and carry none. A module has no row of its own. -The `logicalId` is written through the upstream Alchemy resource that creates the row, as a prop on that resource (ADR-0048). Composer doesn't write it with a separate resource or a later API call. One resource owns each row, so one resource owns its `logicalId`. +One writer owns each row's `logicalId`. For App, Database and Bucket rows, it is the upstream Alchemy resource that creates the row, through its `logicalId` prop (ADR-0048). For the Project, it is container resolution, which creates the Project with its `logicalId` before Alchemy runs and finds it by that `logicalId` on later deploys (ADR-0024). No separate resource or later API call writes a `logicalId`. ## Consequences -- Changing a node's address changes its identity. The node is then a different entity: the platform and the deploy see a new node, and the old one's rows are deleted. Moving a node into another module, or changing its provision ID, does this. Renaming a display name does not. +- Changing a node's address gives it a new identity. The platform and the deploy see a different entity, and the old entity's rows are deleted; for a database or a bucket, that deletes its data. Changing a node's `name` (when `provision()` sets no `id`), changing its provision ID, or moving it into another module all do this. None of these is a rename. +- Changing a display name never changes identity. - The address format is an identity contract. The platform treats `logicalId` as an opaque string and never parses it, so Composer may change how addresses are composed, but any change re-identifies every existing entity. -- Every node kind that lowers to a platform row must pass the node's address as that row's `logicalId`. A new node kind isn't done until it does. -- A `logicalId` already held by another row on the same Branch fails the deploy. Composer never adopts that row. +- Every node kind that lowers to a platform row must pass the node's address as that row's `logicalId`. A new node kind isn't done until it does. An upgrade that gives a resource a `logicalId` prop must pass the address in the same change, or the default writes the Alchemy resource ID. +- Composer must not take over an App, Database or Bucket row because it holds the same `logicalId`. A `logicalId` already held by another row on the same Branch fails the deploy. The Project is the exception: container resolution finds it by `logicalId` on purpose. +- Projects created before they carried a `logicalId` are still found by display name, as a compatibility path. That path is not identity and is not extended to other rows. - Local dev (ADR-0041) creates no platform rows and writes no `logicalId`. ## Alternatives considered @@ -61,14 +63,15 @@ The `logicalId` is written through the upstream Alchemy resource that creates th - **Use Alchemy's default (its resource ID).** No extra code, but `catalog-db` never equals the topology's `catalog`. Every join would fail, and the topology would have to submit Composer's internal resource naming instead of the declaration. - **Write `logicalId` with a separate Composer resource after the row is created.** Duplicates what the upstream resource does (ADR-0048), and puts two resources in charge of one field. When they disagree, they fight on every deploy. - **Put platform `id`s in the topology.** The `id`s don't exist when the topology is written, before the apply. They also differ on every branch, so they can't say "the same service on `main`". -- **Match on display names.** They are mutable and not unique. Matching Projects by display name forked deploy history on rename, which is why Projects got a `logicalId` first. +- **Match on display names.** They are mutable and not unique. Matching Projects by display name forked deploy history when a Project was renamed. - **Have Console read Alchemy's deploy state to map nodes to rows.** That state is Composer's private bookkeeping. Its format belongs to the deploy engine, it contains secrets, and the platform deliberately stores it without reading it. ## Related -- [ADR-0006](ADR-0006-every-node-is-named.md) — every node is named; the root's name names the application. +- [ADR-0006](ADR-0006-every-node-is-named.md) — every node is named; the root's name names the application. Amended by this ADR. - [ADR-0023](ADR-0023-a-prisma-app-is-one-project-a-stage-is-a-branch.md) — a stage is a Branch. +- [ADR-0024](ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md) — container resolution finds the Project. Amended by this ADR. - [ADR-0048](ADR-0048-prisma-cloud-resources-come-from-the-upstream-alchemy-provider.md) — Prisma Cloud resources come from the upstream Alchemy provider. -- [core-model.md § Deployment identity](../10-domains/core-model.md) — the address. -- [alchemy-lowering.md § Platform identity](../05-prisma-cloud/alchemy-lowering.md#platform-identity-logicalid) — which resource carries the `logicalId` for each node kind. +- [core-model.md](../10-domains/core-model.md), "Deployment identity" — the address. +- [alchemy-lowering.md § Platform identity](../05-prisma-cloud/alchemy-lowering.md#platform-identity-logicalid) — which resource carries the `logicalId` for each node kind, and its current status. - [glossary.md § Address and logical ID](../03-domain-model/glossary.md#address-and-logical-id). diff --git a/docs/design/90-decisions/README.md b/docs/design/90-decisions/README.md index 34a839adb..9eeac2420 100644 --- a/docs/design/90-decisions/README.md +++ b/docs/design/90-decisions/README.md @@ -27,7 +27,7 @@ _Earlier drafts (ADR-0001, ADR-0002) were retired as the high-level design settl - [ADR-0003](ADR-0003-deploy-derives-everything-from-the-root-node.md) — `prisma-composer deploy` derives everything from the root node; there is no deploy config file. *(Amended by ADR-0049: the extensions and state store live in the `composer` section of `prisma.config.ts`. Amended by ADR-0050: the command is `prisma deploy `.)* - [ADR-0004](ADR-0004-paths-resolve-relative-to-the-authoring-file.md) — Paths resolve relative to the file that writes them; the build adapter carries the authoring module. - [ADR-0005](ADR-0005-users-build-the-framework-assembles.md) — Users build the app's code; the framework assembles the artifact by documented, deterministic steps (validate, wrap, each app-type's documented deploy step — e.g. Next's static/public copy). No guessing (arithmetic/depth-inference/discovery), no laundering (symlink = hard error); read the build tool's own manifest (Next's `relativeAppDir`), don't walk or compute. *(Superseded in part by ADR-0047: a symlink whose resolved target stays inside the bundle is preserved as a link; the no-guessing and no-dereferencing rules stand.)* -- [ADR-0006](ADR-0006-every-node-is-named.md) — Every node is named; the root's name names the application. *(Amended by ADR-0050: the override is `prisma deploy --name`.)* +- [ADR-0006](ADR-0006-every-node-is-named.md) — Every node is named; the root's name names the application. *(Amended by ADR-0050: the override is `prisma deploy --name`. Amended by ADR-0051: a node's name is its default provision ID, so changing any node's name changes its identity.)* - [ADR-0007](ADR-0007-deploy-drives-alchemy-through-a-generated-stack-file.md) — Deploy drives Alchemy through a generated, inspectable stack file. *(Amended by ADR-0050: `prisma deploy` and `prisma dev` generate the file; teardown is the `destroy` operation.)* - [ADR-0008](ADR-0008-wrapper-inlines-everything-except-runtime-builtins.md) — The boot wrapper inlines everything except runtime built-ins. - [ADR-0009](ADR-0009-deploy-state-is-hosted-in-the-workspace.md) — Deploy state is hosted in the workspace, not in local files. *(Superseded by ADR-0034: still hosted, now per-stage in the app's own Project.)* @@ -45,7 +45,7 @@ _Earlier drafts (ADR-0001, ADR-0002) were retired as the high-level design settl - [ADR-0021](ADR-0021-params-are-read-through-config-not-load.md) — A service reads dependencies through `load()` and config params through a sibling `config()`; the two never share a namespace. *(Superseded by ADR-0042: `config()` and the `params` declaration are replaced by one `input` schema read through `service.input()`; `load()` stays separate.)* - [ADR-0022](ADR-0022-data-deps-carry-a-prisma-orm-contract.md) — Data deps carry a Prisma ORM contract: typed client binding, one contract per database via its config, deploys migrate along authored edges to the contract hash. Revised 2026-08-25: deploys are replay-only — the first-deploy `dbInit` synthesis path is removed; a fresh database replays the committed baseline from empty, and a missing authored path is a refusal naming the fix (`prisma db update` locally, `contract emit` + `migration plan` to ship). *(Proposed)* *(Amended 2026-10-02: a service that uses a database deploys only after that database's migration has completed.)* - [ADR-0023](ADR-0023-a-prisma-app-is-one-project-a-stage-is-a-branch.md) — A Prisma App is one Prisma Cloud Project; Modules are Apps/Databases inside it; a Stage is a Branch, and deploy state is per `(Project, Branch)`. -- [ADR-0024](ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md) — A stage is a deploy-time environment; the CLI resolves the app's Project (by root-module name) and the stage's Branch outside Alchemy before the stack runs. *(Amended by ADR-0050: deploys run as `prisma deploy`; teardown is the `destroy` operation, whose target is required.)* +- [ADR-0024](ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md) — A stage is a deploy-time environment; the CLI resolves the app's Project (by root-module name) and the stage's Branch outside Alchemy before the stack runs. *(Amended by ADR-0050: deploys run as `prisma deploy`; teardown is the `destroy` operation, whose target is required. Amended by ADR-0051: the Project is found by its `logicalId`; display-name matching is a fallback for older Projects.)* - [ADR-0025](ADR-0025-name-the-unit-of-composition-module.md) — The unit of composition is a **Module**, authored with `module()`; supersedes ADR-0014's unit noun ("System"). Registers: package (npm's word) / extension (config slot) / Module (composition). - [ADR-0026](ADR-0026-name-the-framework-prisma-compose.md) — The framework is **Prisma Composer**; "Prisma App" names only the artifact you build. Supersedes ADR-0014's framework, package, and CLI names: `@prisma/composer*` (incl. `compose-alchemy`), `prisma-composer`, `prisma-composer.config.ts`. - [ADR-0027](ADR-0027-two-packages-compose-and-compose-prisma-cloud.md) — Ship two **public** packages: `@prisma/composer` (core + CLI + agnostic subpaths) and `@prisma/composer-prisma-cloud` (target + first-party modules as entrypoints, cron first). Boundary = the user's one choice: where does it run. @@ -72,4 +72,4 @@ _Earlier drafts (ADR-0001, ADR-0002) were retired as the high-level design settl - [ADR-0048](ADR-0048-prisma-cloud-resources-come-from-the-upstream-alchemy-provider.md) — The six Management-API resource families (project, database, connection, app, deployment, environment variable) are the upstream `alchemy/Prisma` provider's classes, registered in Composer's `PrismaComposer` collection; Composer defines resources only where the upstream provider has no support yet (buckets) or no Management API exists behind them. Compute binds the low-level App/Deployment/EnvironmentVariable trio (the `COMPOSER_*_ORIGIN` self-edge and ADR-0005 rule out composite `Compute`); the env→deployment ordering edge rides the deployment's `app` prop, and a deployment is replaced exactly when its artifact or environment fingerprint changes — unchanged services are reused. Legacy rows in the platform state API (retired type-ids and shapes) migrate on read — the older retired SQL state stores stay destroy-only under ADR-0045 — branch-stage databases take generated physical names, and the platform's seeded `DATABASE_URL` is left system-managed. - [ADR-0049](ADR-0049-composers-configuration-is-the-composer-section-of-prisma-config.md) — Composer's configuration is the `composer` section of `prisma.config.ts`, loaded by the Prisma CLI engine. The validator checks only the fields that identify extension and state descriptors, passes descriptors through as the file's own objects, rejects unknown keys, and takes the nearest declaring file's section whole so the generated stack imports exactly one file. The old `prisma-composer.config.ts` and the `configPath` pointer are refused with `CONFIG.SECTION_MISSING`, `CONFIG.FIELD_RETIRED` and `CONFIG.FILE_RETIRED`, never migrated. The validator returns the section with its declaring file, and the programmatic operations take the same `ComposerConfigSource` (`{ value, file }`) and run the same checks. The Alchemy child evaluates the whole `prisma.config.ts`. The effect pre-flight and `DEPS.EFFECT_VERSION_CONFLICT` are retired: a broken tree fails evaluation of `prisma.config.ts` with the engine's `CLI.CONFIG_UNREADABLE`. *(Amended: the alternatives section lists the rejected ways to keep a Composer-owned effect check.)* - [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md) — Composer has no binary of its own: the `prisma` CLI mounts exactly two Composer commands, `prisma deploy ` and `prisma dev `, mounted at the root as bare verbs on purpose rather than under a noun. `destroy` and `log` are operations on `@prisma/composer/control`, called from a script; a teardown script reads `PRISMA_SERVICE_TOKEN` and `PRISMA_WORKSPACE_ID`. Where teardown and logs belong in the `prisma` command tree is not decided; until it is, agents and users need a script for both. A `destroy` command was rejected: the CLI's destructive verb is `delete`, and no Composer command takes `--production`. Retires the `prisma-composer` binary; a CI lint keeps its name out of user-facing files. -- [ADR-0051](ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md) — A node's address is its identity on Prisma Cloud: Composer writes it, byte for byte, as the `logicalId` of the node's one platform row (Project, App, Database, Bucket) and as the node's `logicalId` in the application topology, so the platform joins topology nodes to rows, and one node across branches, by string equality. The value is never an Alchemy resource ID, a display name or a platform `id`; it is passed as a prop on the upstream resource that creates the row, never written by a separate resource. Changing an address changes the entity's identity; changing a display name changes nothing. +- [ADR-0051](ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md) — A node's address is its identity on Prisma Cloud: Composer writes it, byte for byte, as the `logicalId` of the node's one platform row (Project, App, Database, Bucket) and as the node's `logicalId` in the application topology, so the platform joins topology nodes to rows, and one node across branches, by string equality. A provision ID defaults to the node's name, so changing a name changes identity. The value is never an Alchemy resource ID, a display name or a platform `id`; App, Database and Bucket rows get it as a prop on the upstream resource that creates them, and the Project from container resolution; no separate resource writes it. Changing an address changes the entity's identity; changing a display name changes nothing. From f04d98a64a96557b3084038801ca826eb5c407b8 Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 9 Oct 2026 11:46:21 +0200 Subject: [PATCH 3/3] docs(design): correct root-address and rename wording in ADR-0051 Root addresses may contain "-". Changing an identity is never a rename. Changing the root name leaves the old Project in place. Record the remaining follow-ups in the build-reporting plan. Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- .drive/projects/build-reporting/plan.md | 5 +++-- docs/design/03-domain-model/glossary.md | 2 +- .../design/05-prisma-cloud/alchemy-lowering.md | 18 ++++-------------- .../ADR-0006-every-node-is-named.md | 2 +- ...vironment-resolved-to-project-and-branch.md | 2 +- ...ddress-is-its-logical-id-on-the-platform.md | 8 ++++---- 6 files changed, 14 insertions(+), 23 deletions(-) diff --git a/.drive/projects/build-reporting/plan.md b/.drive/projects/build-reporting/plan.md index 6da2d6b92..981832e65 100644 --- a/.drive/projects/build-reporting/plan.md +++ b/.drive/projects/build-reporting/plan.md @@ -117,5 +117,6 @@ This section predates the API shipping. The stack merged, the API is in producti - 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 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 rename the example. +- 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`. diff --git a/docs/design/03-domain-model/glossary.md b/docs/design/03-domain-model/glossary.md index 6c42cd87f..cbf73f24b 100644 --- a/docs/design/03-domain-model/glossary.md +++ b/docs/design/03-domain-model/glossary.md @@ -140,7 +140,7 @@ 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. 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). +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 diff --git a/docs/design/05-prisma-cloud/alchemy-lowering.md b/docs/design/05-prisma-cloud/alchemy-lowering.md index 5c087d8d1..c1a27814e 100644 --- a/docs/design/05-prisma-cloud/alchemy-lowering.md +++ b/docs/design/05-prisma-cloud/alchemy-lowering.md @@ -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 @@ -103,7 +105,7 @@ Alchemy resource itself, since its lifecycle lives outside Alchemy ## Platform identity: `logicalId` -Each node has exactly one platform 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. +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 | | --- | --- | --- | @@ -128,19 +130,7 @@ Status: ## 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 (by `logicalId`, the app name; Projects without one 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. +`@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 diff --git a/docs/design/90-decisions/ADR-0006-every-node-is-named.md b/docs/design/90-decisions/ADR-0006-every-node-is-named.md index c87df872f..5a9731e76 100644 --- a/docs/design/90-decisions/ADR-0006-every-node-is-named.md +++ b/docs/design/90-decisions/ADR-0006-every-node-is-named.md @@ -2,7 +2,7 @@ Amended by [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md): the override is `prisma deploy --name`; the `prisma-composer` binary is retired. The naming rules below are unchanged. -Amended by [ADR-0051](ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md): a node's name is its provision ID unless `provision()` sets an `id`, so by default every node's name is part of its address, and the address is its identity on the platform (`logicalId`). Changing any node's name therefore changes its identity, not just the root's; for a database or bucket that deletes its data. Where this ADR says "renaming" a node, it means changing that identity, not changing a display name. +Amended by [ADR-0051](ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md): a node's name is its provision ID unless `provision()` sets an `id`, so by default every node's name is part of its address, and the address is its identity on the platform (`logicalId`). Changing any node's name therefore changes its identity, not just the root's; below the root, for a database or bucket, that deletes its data. Changing an identity is not a rename: a rename changes a display name and nothing else. Read this ADR's "renaming a root" as "changing the root's name", which points the deploy at a different application. ## Decision diff --git a/docs/design/90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md b/docs/design/90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md index 551e8223e..536d85279 100644 --- a/docs/design/90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md +++ b/docs/design/90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md @@ -2,7 +2,7 @@ Amended by [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md): deploys run as `prisma deploy [--stage ]`. Teardown is the `destroy` operation on `@prisma/composer/control`, whose required `target` is `{ kind: 'stage', stage }` or `{ kind: 'production' }`; there is no destroy command. The `prisma-composer` binary is retired. Stage resolution below is unchanged. -Amended by [ADR-0051](ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md): the CLI finds the Project by its `logicalId`, which is the app name, and creates new Projects with it. Matching by display name, oldest first, remains only as a fallback for Projects created before they carried a `logicalId`. +Amended by [ADR-0051](ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md): the CLI finds the Project by its `logicalId`, which is the app name, and creates new Projects with it. When no Project has that `logicalId`, the CLI falls back to matching by display name, oldest first; this exists for Projects created before they carried a `logicalId`. ## Decision diff --git a/docs/design/90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md b/docs/design/90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md index 187ac1391..5459c2712 100644 --- a/docs/design/90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md +++ b/docs/design/90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md @@ -34,13 +34,13 @@ Prisma Cloud's Project, App, Database and Bucket rows each carry three identifie | `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 | -Composer already has an identity for every node: its **address**. The address is the path of provision IDs from the root, assigned by Load (`auth.api`). A node's provision ID is its `name` unless `provision(node, { id })` sets one, so the name an author writes is, by default, part of the node's identity. The root's direct children have bare addresses, and the root's address is the application's name: the root's name, or `--name` for a deploy (ADR-0006). Provision IDs are letters and digits only, so an address contains only letters, digits and dots. Addresses are unique within a graph, they are derived from code, and git carries them between branches with the rest of the configuration. That is exactly what `logicalId` is for, so the address is the value. The platform's side of this design is the pdp-control-plane [branch topology spec](https://github.com/prisma/pdp-control-plane/blob/main/projects/branch-topology/spec.md). +Composer already has an identity for every node: its **address**. The address is the path of provision IDs from the root, assigned by Load (`auth.api`). A node's provision ID is its `name` unless `provision(node, { id })` sets one, so the name an author writes is, by default, part of the node's identity. The root's direct children have bare addresses, and the root's address is the application's name: the root's name, or `--name` for a deploy (ADR-0006). Provision IDs are letters and digits only, so below the root an address contains only letters, digits and dots. Addresses are unique within a graph, they are derived from code, and git carries them between branches with the rest of the configuration. That is exactly what `logicalId` is for, so the address is the value. The platform's side of this design is the pdp-control-plane [branch topology spec](https://github.com/prisma/pdp-control-plane/blob/main/projects/branch-topology/spec.md). The platform depends on the two copies being equal. The application topology Composer submits on each deploy names every node by `logicalId` and contains no platform `id`s, because the rows don't exist yet when it is written. Console and CI then join topology nodes to rows by plain string equality on `logicalId`. A node with no matching row was declared but not created. A row whose `logicalId` matches no node is no longer declared. A row with no `logicalId` matches nothing. Comparing a database on a preview branch with its counterpart on `main` is the same lookup on two branches. If the row's `logicalId` differs from the node's by even one character, every one of these reads silently finds nothing. So the value must be the address, not something that resembles it. In particular, it is never: -- **an Alchemy resource ID.** Alchemy is the deploy engine Composer lowers to. Composer names Alchemy resources after the address plus a role suffix (`catalog-db`, `catalog-conn`, `web-svc`), because one node lowers to several resources. Those names are Composer's internal naming and appear nowhere in the topology. A value containing `-` is never an address, so it is always one of these. Alchemy's own documentation calls its resource ID a "logical ID". That is a different value from the platform's `logicalId`, and the shared words must not be read as the same thing. Upstream Alchemy's Prisma resources default `logicalId` to their fully qualified resource ID, which for Composer is the resource ID, so Composer always passes `logicalId` explicitly. +- **an Alchemy resource ID.** Alchemy is the deploy engine Composer lowers to. Composer names Alchemy resources after the address plus a role suffix (`catalog-db`, `catalog-conn`, `web-svc`), because one node lowers to several resources. Those names are Composer's internal naming and appear nowhere in the topology. Below the root an address never contains `-`, so a value like `catalog-db` on an App, Database or Bucket row is always one of these. Alchemy's own documentation calls its resource ID a "logical ID". That is a different value from the platform's `logicalId`, and the shared words must not be read as the same thing. Upstream Alchemy's Prisma resources default `logicalId` to their fully qualified resource ID, which for Composer is the resource ID, so Composer always passes `logicalId` explicitly. - **a display name.** It is a label. Composer often derives it from the address, so the two can look alike, but it carries no identity. - **a platform `id`.** It differs on every branch and doesn't exist until the row does. @@ -50,12 +50,12 @@ One writer owns each row's `logicalId`. For App, Database and Bucket rows, it is ## Consequences -- Changing a node's address gives it a new identity. The platform and the deploy see a different entity, and the old entity's rows are deleted; for a database or a bucket, that deletes its data. Changing a node's `name` (when `provision()` sets no `id`), changing its provision ID, or moving it into another module all do this. None of these is a rename. +- Changing a node's address gives it a new identity, and the platform and the deploy see a different entity. Changing a node's `name` (when `provision()` sets no `id`), changing its provision ID, or moving it into another module all do this. None of these is a rename. Below the root, the deploy deletes the old entity's rows; for a database or a bucket, that deletes its data. For the root, the new name resolves to a different Project, and the old Project is left in place. - Changing a display name never changes identity. - The address format is an identity contract. The platform treats `logicalId` as an opaque string and never parses it, so Composer may change how addresses are composed, but any change re-identifies every existing entity. - Every node kind that lowers to a platform row must pass the node's address as that row's `logicalId`. A new node kind isn't done until it does. An upgrade that gives a resource a `logicalId` prop must pass the address in the same change, or the default writes the Alchemy resource ID. - Composer must not take over an App, Database or Bucket row because it holds the same `logicalId`. A `logicalId` already held by another row on the same Branch fails the deploy. The Project is the exception: container resolution finds it by `logicalId` on purpose. -- Projects created before they carried a `logicalId` are still found by display name, as a compatibility path. That path is not identity and is not extended to other rows. +- When no Project has the app name as its `logicalId`, container resolution falls back to matching a Project by display name. This is a compatibility path for Projects created before they carried a `logicalId`. That path is not identity and is not extended to other rows. - Local dev (ADR-0041) creates no platform rows and writes no `logicalId`. ## Alternatives considered