Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
08cc2f1
docs(drive): platform-variables slice 2 spec and dispatch plan
wmadden-electric Nov 6, 2026
b31a0b9
feat(prisma-cloud): the input document carries $platform markers, and…
wmadden-electric Nov 6, 2026
29006e9
feat(prisma-cloud): one checker for platform variable names, and an e…
wmadden-electric Nov 6, 2026
cfdb120
feat(prisma-cloud): deploy preflight writes no variable, and fails on…
wmadden-electric Nov 6, 2026
43671a5
feat(prisma-cloud): the deploy lists every platform variable with no …
wmadden-electric Nov 6, 2026
2c519c0
fix(prisma-cloud): no doc comment or message says the deploy fills a …
wmadden-electric Nov 6, 2026
b110774
docs(prisma-cloud): ADR-0064, and every record says the deploy fills …
wmadden-electric Nov 6, 2026
0800bd6
docs(drive): slice 2 spec says what preflight runs after, and which r…
wmadden-electric Nov 6, 2026
45706b5
ci(e2e): the storefront deploy job sets AUTH_SIGNING_SECRET on the pl…
wmadden-electric Nov 6, 2026
0ba55b1
fix(prisma-cloud): core carries the preflight findings to the deploy …
wmadden-electric Nov 6, 2026
168f2fa
docs(prisma-cloud): records say how the findings reach the deploy rep…
wmadden-electric Nov 6, 2026
01cb893
fix(cli): redaction keeps the <value> placeholder in a command that s…
wmadden-electric Nov 6, 2026
c6d9b2d
style(cli): format the redaction test
wmadden-electric Nov 6, 2026
8b553a8
fix(cli): redaction keeps only an exact <value> placeholder
wmadden-electric Nov 6, 2026
48b5625
Merge remote-tracking branch 'origin/main' into claude/platform-varia…
wmadden-electric Nov 6, 2026
f6f5df6
fix(prisma-cloud): round-2 review fixes for slice 2
wmadden-electric Nov 6, 2026
a000253
test(integration): an unset per-key param with a default no longer st…
wmadden-electric Nov 6, 2026
3b45c3a
fix(prisma-cloud): round-3 review fixes for slice 2
wmadden-electric Nov 6, 2026
2717472
Merge remote-tracking branch 'origin/main' into claude/platform-varia…
wmadden-electric Nov 10, 2026
95632e0
docs(prisma-cloud): say the deploy does not copy local values to plat…
wmadden-electric Nov 10, 2026
3ac355d
ci(e2e): the first-deploy check is a script, not inline shell
wmadden-electric Nov 10, 2026
ee044ad
docs(drive): review questions that catch shortcuts, comments, inline …
wmadden-electric Nov 10, 2026
c586b2b
ci(e2e): match variable names as words instead of building a regular …
wmadden-electric Nov 10, 2026
5de000e
test(e2e): prove the signing secret reached auth with an HMAC of a ra…
wmadden-electric Nov 10, 2026
f1a1b6d
docs(platform-variables): state the deploy choice as current and in p…
wmadden-electric Nov 10, 2026
8d815b3
fix(input-document): plain boot errors, and $secret is no longer escaped
wmadden-electric Nov 10, 2026
762c480
fix(core): plain wording in preflight errors and test names
wmadden-electric Nov 10, 2026
4a08e0f
fix(prisma-cloud): one formatter for missing variables, no re-exports…
wmadden-electric Nov 10, 2026
b1c1e3d
fix(cli): set the findings variable through preflightFindingsEnv on e…
wmadden-electric Nov 10, 2026
72dc631
test(storefront-auth): prove the signing key with a key check value, …
wmadden-electric Nov 10, 2026
1fddd57
ci(e2e): one script runs the first deploy and sets the platform varia…
wmadden-electric Nov 10, 2026
a348f6c
docs(platform-variables): quote the rewritten param error and the at-…
wmadden-electric Nov 10, 2026
ac2414d
test(integration): prisma dev names a missing variable with "read by …
wmadden-electric Nov 10, 2026
d9eb1c1
Merge remote-tracking branch 'origin/main' into claude/platform-varia…
wmadden-electric Nov 10, 2026
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
11 changes: 9 additions & 2 deletions .drive/projects/platform-variables/deferred.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,24 @@ Each needs a Management API change. None blocks this project (design section 3).
- **A platform record of which variables each service reads.** Composer's input documents already name them; the target would also send them to the platform, which could then deliver only those. Until then every deployment receives every row of its class.
- **A service scope.** Today a name resolves at two scopes, project class and branch, and preflight lists both. Will expects the platform to add a service scope. When it does, preflight's existence check gains one list call per reader; nothing else in the deploy changes, because the node never says where its value lives and preflight is the only part of the deploy that knows which scopes exist. `prisma dev` supplies one value per name from local files, so reproducing two readers with two values locally needs a per-reader local source, decided when the scope exists.
- **A void value.** Let an operator set a variable to "deliberately nothing", so the deploy report stops listing an intended absence as not set. Today the platform refuses an empty value.
- **Workspace variables and rules.** Values shared across projects, with rules for which branches see them. Composer creates no values, so this needs nothing from Composer.
- **Workspace variables and rules.** Values shared across projects, with rules for which branches see them. Composer does not create platform variable values today, so this needs nothing from Composer.
- **A topology entry for a platform variable node.** The platform's application topology has a fixed list of node kinds, owned by the Portal team. The node is not sent to it until the platform has a kind for it.

## Later Composer work

- **Destroying production deletes the operator's platform variables.** Destroying an app's production environment deletes its Prisma Cloud project, and with it every variable the operator set; the platform never returns a value, so an operator who kept no copy must make a new secret. Options: keep the project when it holds rows that are not Composer's, or warn before deleting. Waits because destroy's behaviour is not this slice's and needs its own decision.
- **An author-declared per-reader variable,** one that maps to the service scope the platform is expected to add, so that two readers of one name are guaranteed different values. Not built: where a value lives is the operator's choice on the platform, and nobody has a case that needs the author to force it. Wait for a real case.
- **A warning when the app names a variable another module expects under a different name.** Not needed: the deploy report lists every name with its readers, so a module wired to the wrong name shows up there.
- **An edge kind for "a service reads a platform variable".** Core's edges are dependencies (ADR-0063 names two relationships: depends on, exposes). The reads relation is recorded as a list beside each service's binding, not as an edge, because adding an edge kind changes core's graph types and the platform's edge families at once. Reason to wait: nothing reads the relation as an edge yet; the deploy report and errors read the list.
- **`schema` on a platform secret.** A secret box whose content is not text is undefined in this design and in module-input's. A `secret(inner)` field already checks a secret's text at boot. Needs a generic box type first.
- **Removing `envSecret()` and `envParam()`** after their deprecation.
- **A seeding command** that copies values from local files to the platform. Shelved by Will on 2026-11-05.
- **An explicit way to set platform variables through `deploy`.** The operator names the values and the environments they go to, and the deploy writes those rows. Expected later. Not now: it needs its own design, and an implicit copy of local values is what slice 2 removed.
- **Isolating a service's environment on Prisma Cloud** without a platform record of who reads what, for example by having boot remove variables a service does not read. Not planned: it would hide variables an app reads directly from `process.env`.
- **Reading a deployment's logs from `prisma deploy`.** Composer does not tail deployment logs, so a boot error on the platform is read with `prisma service logs <service>`. The guide says so; tailing is a later change.
- **A value given by a resource that also provides a contract.** `provision(resource)` flattens a contract onto the returned object, so a `value` member would collide. Allowed only on a resource with no contract until a second case needs both.
- **A separate export for `envParam()` in per-key params.** One function serves both forms today, so its `@deprecated` tag strikes through the per-key form too, which is not deprecated. A second export, with the deprecation only on the binding form, gives a clean signal. Deferred from slice 2's code review (F03): the replacement for the binding form arrives with slice 3.
- **Renaming `absentSecrets`** in the serializer and the dev payload to say unset variables. In slice 2 every document marker is an `envSecret()`, so the name is accurate. Slice 3 adds markers for plain variables, and the rename belongs there.
- **A report channel for preflight checks that are not about platform variables.** `PreflightInput.report` carries platform-variable findings and a count only. Designing a general channel waits for a second preflight check that needs one.
- **The CI script's deep imports from source paths.** `scripts/e2e-set-platform-variables.ts` imports from `packages/1-prisma-cloud/0-lowering/lowering/src/`, as four other scripts already do. Changing it is a change to that existing practice, not to this project.
- **The platform variables with no value in the `--report` run report.** The printed deploy report and the JSON output carry the list; the run report file holds steps, timings and the failure cause, and does not. Found in slice 2 QA. Reason to wait: no CI consumer reads the list from that file yet.
- **The one-meaning-per-name check misses an `envParam()` passed indirectly.** The check sees `envParam()` only in a service binding or in per-key params. An `envParam()` passed through a module's input or a `resolve()` dependency becomes text at Load, so a clash with a root input field of the same name is not caught. Fixing it needs Load to keep the source's name and location, which is a core change outside slice 2. Found in PR #426 review.
16 changes: 9 additions & 7 deletions .drive/projects/platform-variables/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default appRoot('shop', {}, ({ provision }) => {
- `EMAIL_DELIVERY_CREDENTIAL` is a secret. The service receives it in a secret box.
- `SMTP_PORT` holds the text `587` on the platform. `schema` says the text is a number, so the service's `smtpPort` field, typed `number`, receives `587`.

The deploy never reads, writes or needs any of these values. A missing value fails the deploy only when a field that must have a value reads it; otherwise the deploy lists the variable as not set and succeeds. The values are set on the platform.
The deploy does not copy any of these values from the developer's local environment, and does not read or need them. A missing value fails the deploy only when a field that must have a value reads it; otherwise the deploy lists the variable as not set and succeeds. For now the operator sets the values on the platform.

The two shortcuts become spellings of the same thing. `envSecret('NAME')` is a platform secret written inline, with the meaning of `variable.secret({ name: 'NAME' })` provisioned in the root, and follows every rule in this document. `envParam('NAME')` in an input binding is a deploy-time value from the app configuration, not a platform variable, and is deprecated with `envSecret()`; a root input field replaces it. Section 12 has the detail.

Expand Down Expand Up @@ -135,7 +135,7 @@ This reverses the glossary's statement that "there is no separate root construct

### What the node is

- **The node provisions nothing and owns nothing on the platform.** It names a variable. Its descriptor writes no row, creates no Alchemy resource, and is not written to the platform's application topology. The variable's row is the operator's, and Composer never writes on it: not a value, not a `logicalId`, nothing. The node is the app's reference to something managed elsewhere, as a Terraform data source is.
- **The node provisions nothing and owns nothing on the platform.** It names a variable. Its descriptor writes no row, creates no Alchemy resource, and is not written to the platform's application topology. The variable's row is the operator's, and Composer does not write on it: not a value, not a `logicalId`, nothing. The node is the app's reference to something managed elsewhere, as a Terraform data source is.
- **The variable's identity is its name; the node's identity is its address.** `name` is a property of the node. The node's address is the path of provision ids from the root, as for every node.
- **One declaration per name.** Inside the root, two `variable()` calls with one `name` fail Load, naming both ids; so do a plain and a secret with one name. This cannot be structural, because the two calls are two lines in the root's body, but a collision is always between two lines the app's author wrote and can fix. The same node object provisioned twice is two declarations of one name, and fails the same way.
- **`provision()` returns the value itself.** Provisioning the node returns a runtime value with `id` on it, not a wrapper: `const from = provision(variable({ name: 'EMAIL_FROM' }))` gives a `RuntimeValue<string> & { id: string }`, which a binding uses directly. The precedent is `generatedParam()` and `serviceOrigin()`, which each return one object that is both source and value. The pointer names the variable: a plain pointer for `variable()`, a secret pointer for `variable.secret()`.
Expand Down Expand Up @@ -195,6 +195,8 @@ A platform variable's name must match `^[A-Z][A-Z0-9_]*$`: it starts with an upp

Upper case only, because the platform accepts no other key (section 3), and because the two places a name is used do not agree about case: Composer compares pointer names without case, and a process environment on Linux compares them with case. With upper-case names a row and a name can never differ by case, so boot always finds the row preflight saw.

No leading underscore, though the platform accepts one. Shells and tools use names that start with an underscore for their own variables: bash puts `_` in the environment of every command it runs, and the Java virtual machine reads `_JAVA_OPTIONS`. The rule is Composer's, and the error says so.

### Reserved names

The constructor refuses these names, saying why:
Expand Down Expand Up @@ -305,7 +307,7 @@ An empty value cannot exist on the platform (section 3), so at boot a variable i

## 9. Deploy

- **The deploy writes no variable.** Preflight no longer creates a missing variable from the app secrets or the app configuration. That was seeding, and with rows per class and per branch, seeding as a side effect of `deploy` is how a developer's local test key reaches production. A variable is set on the platform.
- **The deploy does not copy local values to platform variables.** Preflight no longer creates a missing variable from the app secrets or the app configuration. An implicit copy can push a local value into a remote environment by accident: `.env` is read for every environment, and copying on each deploy also writes one override row per preview branch. For now the operator sets each value on the platform with `prisma project env add`, and preflight only checks that what a service needs is there. This is not a rule that the deploy may never set a variable: an explicit way to set variables through `deploy`, where the operator says which values go to which environment, is expected later (deferred.md).
- **Preflight reads existence, never a value.** For each platform variable the graph reads, preflight asks whether a row exists at any scope one of its readers resolves from, following the platform's own resolution: the environment's project class and the environment's branch, the two scopes the platform offers today (section 3). A row at either counts as existing, and preflight keeps the `updatedAt` of the row the platform resolves: the branch row when there is one, otherwise the class row. A change to the class row while a branch override exists does not change what the service receives, so it is not a trigger. The reads run in parallel, at most eight at a time; a failed list call still fails the deploy, because the triggers need `updatedAt`. Preflight does not list every row of the class, because in the preview class that returns every branch's `COMPOSER_` rows. When the platform adds a service scope, preflight gains one list call per reader, and nothing else in the deploy changes.
- **A missing variable that a service needs fails the deploy.** A deployment is started and promoted as soon as it is created, and the platform reports a process that exits at boot as running (section 3), so a service that exits at boot is promoted while crashing and the deploy succeeds. Preflight prevents the case it can know about: for each service, it first runs the service's schema with no variable left out, and a failure there is an invalid input, reported as such and never blamed on a variable. Then it resolves the input binding with every variable that has no row left out and runs the schema again. If the schema passes, those variables are optional. If it fails, preflight checks each one alone and names as required the variables whose absence alone fails; when no single one fails alone, it says the service needs some of them. A marker in an array position is required without a check. This is the method ADR-0058 gives the dev preflight, and it never needs a value. The error lists each required variable, the service and field that read it, and the command that sets it.
- **A missing variable nothing needs is reported.** The deploy succeeds, and the variable is listed under "Platform variables with no value", with each service that reads it and the command that sets it.
Expand Down Expand Up @@ -374,7 +376,7 @@ The graph is still a composition of modules that are all the same, except the ro
- **Glossary, every "implicit root Module" passage** (the two Aggregate Contract passages and the superseded-terms arrow). Now "app root".
- **Glossary, new entry App root:** the outermost module, the one the entry default-exports and nothing provisions, built with `appRoot()`. Also called the root, the root module or the root node; those terms stay where they are exact and point at this entry.

### Slice 2: the deploy fills nothing
### Slice 2: the deploy stops copying local values

- **Glossary, The app secrets, Secrets key, Named secrets, Known secrets:** each defines its set through `envSecret()` and says "Deploy preflight fills a missing `envSecret()` platform variable". Now the app secrets fill nothing on the platform, and each set is defined through the names the graph's secret sources use. Slice 3 redefines them through platform secrets.
- **Glossary, The app configuration:** "Deploy preflight also fills a missing platform variable" is gone. Under `prisma dev` the local target fills a plain platform variable from it (slice 3 adds that sentence).
Expand All @@ -384,11 +386,11 @@ The graph is still a composition of modules that are all the same, except the ro
- **ADR-0042, service input:** the `$secret` marker; "Every variable a service's markers name must be distinct once normalised"; the three boot outcomes. Now the `$platform` marker, distinct names for `$generated` only, the four boot steps, the empty-text rule, and the array-position rule.
- **ADR-0058, dev secrets:** "Each service gets only the variables its own rows point at" is stated as a local-target rule, not a Prisma Cloud one; and the recorded difference between dev's and deploy's input documents for an unset optional secret closes, because both now leave the key out.
- **ADR-0062, values and config adapters:** "Deploy preflight and dev preflight fill a missing `envSecret()` platform variable". Now preflight fills nothing. Slice 3 rewrites the Values section.
- **ADR-0064, new, "A platform variable is a node":** created with the platform facts of section 3, the graph check hook, the preflight findings, and sections 8 to 12 as they apply to every operator variable. Slice 3 adds the node.
- **ADR-0064, new, "The deploy does not copy local values to platform variables":** created with the platform facts of section 3, the graph check hook, the preflight findings, and sections 8 to 12 as they apply to every operator variable. Slice 3 adds the node.

### Slice 3: a platform variable is a node

- **Glossary, Core nouns:** "Anything without a lifecycle — an API key, an external URL — is part of a node's Input, not a node." Now a node either has a lifecycle Composer manages, or refers to something the platform owns; Composer lists a referenced value and never creates it.
- **Glossary, Core nouns:** "Anything without a lifecycle — an API key, an external URL — is part of a node's Input, not a node." Now a node either has a lifecycle Composer manages, or refers to something the platform owns; Composer lists a referenced value and does not create it.
- **Glossary, Resource:** "A node whose lifecycle a provider manages." Now also a node that refers to a value the platform owns.
- **Architectural principles, "A node's address is its identity, everywhere," and ADR-0051:** a node's one identity is its address, written as the `logicalId` of its platform row. Now a platform variable node, which has no row and names something Composer does not own, is identified by the variable's name; the principle gains that one exception, with its reason.
- **Glossary, Topology:** "Every node in the topology has a managed lifecycle." Now every node either has one or refers to something the operator sets, and the second kind is listed by name when an environment is created.
Expand Down Expand Up @@ -430,7 +432,7 @@ No two slices rewrite one record at the same time: each slice starts after the o
- **`schema` on a platform secret.** Deferred: a secret box whose content is not text is undefined in both this design and module-input's; `secret(inner)` already checks a secret's text at boot.
- **Run the module's schema at boot.** Impossible: the schema is code in the app's module, which is not in the service's bundle.
- **Let the platform validate.** Will expects the platform never to validate values against a schema. Composer validates at boot and in `prisma dev`.
- **Keep seeding on deploy, or add a seeding command.** Seeding on deploy is how a local value reaches production. A seeding command was shelved by Will on 2026-11-05.
- **Keep copying local values on deploy.** An implicit copy is how a local value reaches a remote environment by accident. An explicit way to set variables through `deploy` is expected later (deferred.md).
- **Make `envSecret()` a node provisioned where it is written.** Rejected: Load would have to create a node from a value found in a binding, with an owner and an id nobody has defined, for a form the same slice deprecates.
- **"Subscription" as the word for a service reading a variable.** Rejected: it names a platform mechanism that does not exist, reads as "receives updates as they happen", which is false, and already means streaming in the principles.
- **Wait for the platform to record which service reads which variable.** Not needed: every part ships against today's API (section 3), and the platform's record would change nothing an author writes.
Loading
Loading