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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions docs/design/03-domain-model/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,23 +255,23 @@ The name in the app configuration that a param source reads: `envParam('NAME')`

### The app secrets

The merged values the `secrets` adapters return, text only. The name a secret source reads is its **secrets key** (`secretSource()`'s `secretsKey`; `envSecret('NAME')` passes `NAME`). Deploy preflight fills a missing `envSecret()` platform variable from it. `prisma dev` hands its second process only the **named secrets**, the values under the secrets keys the graph's `envSecret()`s name, in one environment variable, `PRISMA_COMPOSER_NAMED_SECRETS` (in code: `NamedSecrets`). The app secrets hold values Composer may carry; only the values under the secrets keys the graph names become known secrets. `prisma dev` compares the whole app secrets between rebuilds to notice a change, which reads them without making them known secrets. Composer never passes the app secrets to Load or a schema, never writes their values to a file, and redacts the values under the secrets keys wherever it prints. With no `secrets` list it is the process environment. In code: `AppSecrets`.
The merged values the `secrets` adapters return, text only. The name a secret source reads is its **secrets key** (`secretSource()`'s `secretsKey`; `envSecret('NAME')` passes `NAME`). Deploy preflight fills a missing `envSecret()` platform variable from it. `prisma dev` hands the local providers only the **named secrets**, the values under the secrets keys the graph's `envSecret()`s name, as a value in memory (in code: `NamedSecrets`). The app secrets hold values Composer may carry; only the values under the secrets keys the graph names become known secrets. `prisma dev` compares the whole app secrets between rebuilds to notice a change, which reads them without making them known secrets. Composer never passes the app secrets to Load or a schema, never writes their values to a file, and redacts the values under the secrets keys wherever it prints. With no `secrets` list it is the process environment. In code: `AppSecrets`.

### Secrets key

The name in the app secrets that a secret source reads: `envSecret('NAME')` reads `NAME`. See [the app secrets](#the-app-secrets). In code: `secretSource()`'s `secretsKey`, and `secretsKeys(graph)`, every secrets key a graph reads.

### Named secrets

The app secrets under the secrets keys the graph reads, each with text. Only `prisma dev` hands them to its second process, in one environment variable. See [the app secrets](#the-app-secrets). In code: `NamedSecrets`, `PRISMA_COMPOSER_NAMED_SECRETS`.
The app secrets under the secrets keys the graph reads, each with text. Only `prisma dev` hands them on, to the local providers, as a value in memory. See [the app secrets](#the-app-secrets). In code: `NamedSecrets`.

### Known secrets

The values one command knows are secrets: the value under each secrets key the graph's `envSecret()`s name, in the app secrets or the app configuration. Nothing else is a known secret: a plain value in the same `.env` file, or the shell's `SHLVL=1` under `secrets: [dotenv(), environmentVariables()]`, is not. The CLI refuses to write the app configuration file when a value in it contains a known secret: one of eight characters or more anywhere in the value, a shorter one only as the whole value. The CLI redacts the known secrets wherever it prints, together with **credential-looking values**: text of eight characters or more under a variable name that looks like a credential's, which might not be secrets ([ADR-0062](../90-decisions/ADR-0062-environment-values-come-from-resolve-and-config-adapters.md)). In code: `KnownSecrets`, and `SecretRedaction`, which holds both kinds.

### The app configuration file

The JSON file a command's first process writes for its second process. It holds what Load read from the app configuration (the root input fields the root's body passes on, and the text of each configuration key the graph reads) and the first process's node addresses. It never holds a secret: Load and the CLI refuse to write one into it. Destroy writes it to `.prisma-composer/app-configuration-<pid>-<uuid>.json` and deletes it when the run ends. Deploy writes none: it runs Alchemy in its first process and hands `lower()` what Load read as a value (ADR-0064). `prisma dev` writes it to `<dev environment dir>/app-configuration-<pid>.json` before each converge and deletes it as soon as that converge ends. Only its owner can read it. The CLI also deletes it when its process exits. A file left by a process killed outright is deleted at the start of a later deploy, destroy or `prisma dev`, once it is ten minutes old. It is not `prisma.config.ts`, which is "the config file". In code: `PRISMA_COMPOSER_APP_CONFIGURATION_FILE`, `AppConfigurationRead`, `readAppConfigurationFile`.
The JSON file a command's first process writes for its second process. It holds what Load read from the app configuration (the root input fields the root's body passes on, and the text of each configuration key the graph reads) and the first process's node addresses. It never holds a secret: Load and the CLI refuse to write one into it. Destroy writes it to `.prisma-composer/app-configuration-<pid>-<uuid>.json` and deletes it when the run ends. Deploy and `prisma dev` write none: they run Alchemy in their first process and hand `lower()` what Load read as a value (ADR-0064). Only its owner can read it. The CLI also deletes it when its process exits. A file left by a process killed outright is deleted at the start of a later deploy, destroy or `prisma dev`, once it is ten minutes old. It is not `prisma.config.ts`, which is "the config file". In code: `PRISMA_COMPOSER_APP_CONFIGURATION_FILE`, `AppConfigurationRead`, `readAppConfigurationFile`.

### Config adapter

Expand Down Expand Up @@ -412,7 +412,11 @@ to a plan.

### Converge

Making an environment match the lowered stack: Alchemy plans what to create, update, replace and delete, then applies it. `prisma deploy` converges in its own process through Alchemy's programmatic API; `prisma dev` and `destroy` converge in an Alchemy child process. The *app converge* is the app install's `lower` and in-process converge, which `deploy` loads from `@prisma/composer/internal/converge` ([ADR-0064](../90-decisions/ADR-0064-deploy-runs-alchemy-in-process.md)). Codes: `DEV.CONVERGE_FAILED`, `DEPLOY.CONVERGE_RUNNING`, `DEPS.CONVERGE_UNRESOLVABLE`, `DEPS.CONVERGE_CONTRACT_MISMATCH`.
Making an environment match the lowered stack: Alchemy plans what to create, update, replace and delete, then applies it. `prisma deploy` and `prisma dev` converge in their own process through Alchemy's programmatic API; `destroy` converges in an Alchemy child process. The *app converge* is the app install's `lower` and in-process converge, with the local target's providers and state store for dev, which `deploy` and `dev` load from `@prisma/composer/internal/converge` ([ADR-0064](../90-decisions/ADR-0064-deploy-runs-alchemy-in-process.md)). Codes: `DEV.CONVERGE_FAILED`, `DEV.INTERRUPTED`, `DEV.CONVERGE_RUNNING`, `DEV.WORKER_THREAD`, `DEPLOY.CONVERGE_RUNNING`, `DEPS.CONVERGE_UNRESOLVABLE`, `DEPS.CONVERGE_CONTRACT_MISMATCH`.

### App definition files

The files that define an app for `prisma dev`: every module outside `node_modules` that importing the entry module loaded, static and dynamic imports alike, plus each service's authoring module. Dev imports them once per process, so an edit to one cannot reach a rebuild; dev reports it (`definition-changed`) and pauses rebuilds until it is restarted ([ADR-0064](../90-decisions/ADR-0064-deploy-runs-alchemy-in-process.md)).

### Control plane / Execution plane

Expand Down
2 changes: 1 addition & 1 deletion docs/design/10-domains/core-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ interface LowerOptions {
readonly name: string // stack name (+ Load's root id override)
readonly environment?: string // the environment being deployed, as --environment gave it;
// Load's environment.name. The generated stack files always pass it.
readonly appConfigurationRead?: AppConfigurationRead // passed to Load and the serializer; deploy passes its own Load's, the generated stack files read it from the app configuration file
readonly appConfigurationRead?: AppConfigurationRead // passed to Load and the serializer; deploy and dev pass their own Load's, the generated destroy stack file reads it from the app configuration file
readonly firstProcessNodeAddresses?: readonly string[] // from the same file; lowering refuses a graph whose addresses differ
// `prisma deploy` runs each service's build-adapter assembler and writes
// the resulting bundle dirs here, into the generated stack file it hands to
Expand Down
10 changes: 5 additions & 5 deletions docs/design/10-domains/deploy-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ that relative imports inside the entry graph may use `./x.js` or extensionless
bun APIs can only deploy under bun, since loading the graph imports that module
— the app's choice, not a CLI limit.

**Where Alchemy runs.** `prisma deploy` runs Alchemy in the `prisma` process, through Alchemy's programmatic API (`alchemy/Alchemist`) ([ADR-0064](../90-decisions/ADR-0064-deploy-runs-alchemy-in-process.md)). It loads core's `lower()` and the converge from `@prisma/composer/internal/converge`, resolved from the directory of the app's `prisma.config.ts`, so they use the app's own install of `alchemy` and `effect`, the same one its extensions and providers come from. The `prisma` host's Composer calls that install, so the two must speak the same converge contract, a number the install exports from `@prisma/composer/internal/converge-contract`; their package versions may differ. A different contract fails with `DEPS.CONVERGE_CONTRACT_MISMATCH`, naming both contracts and both versions and saying which side to upgrade. An app whose `@prisma/composer` cannot be resolved, or predates that entry, fails with `DEPS.CONVERGE_UNRESOLVABLE`. Both fail before anything is created. Alchemy and `effect` load only behind the deploy operation's lazy executor import, so `prisma --version` never loads them.
**Where Alchemy runs.** `prisma deploy` and `prisma dev` run Alchemy in the `prisma` process, through Alchemy's programmatic API (`alchemy/Alchemist`) ([ADR-0064](../90-decisions/ADR-0064-deploy-runs-alchemy-in-process.md)). It loads core's `lower()` and the converge from `@prisma/composer/internal/converge`, resolved from the directory of the app's `prisma.config.ts`, so they use the app's own install of `alchemy` and `effect`, the same one its extensions and providers come from. The `prisma` host's Composer calls that install, so the two must speak the same converge contract, a number the install exports from `@prisma/composer/internal/converge-contract`; their package versions may differ. A different contract fails with `DEPS.CONVERGE_CONTRACT_MISMATCH`, naming both contracts and both versions and saying which side to upgrade. An app whose `@prisma/composer` cannot be resolved, or predates that entry, fails with `DEPS.CONVERGE_UNRESOLVABLE`. Both fail before anything is created. Alchemy and `effect` load only behind the operations' lazy executor imports, so `prisma --version` never loads them. `prisma dev` loads the local target's providers and state store from the same entry, and converges once at start and once per rebuild, one at a time ([local-dev.md](local-dev.md) § Pipeline).

**The Alchemy child's runtime (`destroy` and `prisma dev`).** These still start an Alchemy child. Composer runs the `bin` of the `alchemy`
**The Alchemy child's runtime (`destroy`).** `destroy` still starts an Alchemy child. Composer runs the `bin` of the `alchemy`
package that the app's `@prisma/composer` depends on, found from the app
directory the way the generated stack file's own imports are, so the CLI and
the stack code come from one install. It starts that bin with Node, as the
Expand Down Expand Up @@ -192,7 +192,7 @@ user can rerun it by hand (ADR-0007).
- **Process state.** For the length of the plan and apply, `deploy` sets each reporter's variables in `process.env`, and changes to the deploy's working directory when the process is elsewhere, because Alchemy reads `.env` and keeps `.alchemy` there. It restores both on every exit path. One process runs one converge at a time, across every install it loads (`DEPLOY.CONVERGE_RUNNING`), and a worker thread cannot change directory (`DEPLOY.WORKER_THREAD`).
- **Credentials.** The command passes the engine's `ctx.api` and the active credential's workspace as the operation's `credentials`, and `lower()` receives them as `LowerOptions.credentials`. Every Management API request the deploy makes goes through that client, which sets the bearer token and refreshes it: Composer's own resources call it, and Alchemy's Prisma providers and state store reach it through an Effect `HttpClient` that forwards Management API requests to it without their own `Authorization` header and sends other hosts (presigned upload URLs) to node's transport. The client's host comes from `PRISMA_MANAGEMENT_API_URL`; a `PRISMA_API_URL` that names another host fails the deploy with `DEPLOY.MANAGEMENT_API_URL_CONFLICT` before anything is created. No token reaches Composer's code and none is written into `process.env`, so a `prisma login` session deploys like `PRISMA_SERVICE_TOKEN` ([ADR-0064](../90-decisions/ADR-0064-deploy-runs-alchemy-in-process.md)). A caller of the operation that passes no credentials gets a client built from `PRISMA_SERVICE_TOKEN`.

`destroy` (and `prisma dev`) write the pipeline's results as a runnable stack module at `.prisma-composer/alchemy.run.ts` and drive the `alchemy` CLI against it (ADR-0007), started as § Runtime describes, carrying every resolved container across to the child as one environment variable per extension — content the CLI writes but never reads (ADR-0038). `alchemy destroy` re-imports and re-evaluates the same stack, so its target reconstruction needs the containers. The generated file and Alchemy's state live in the process's working directory — tool state lives where you run the tool, like any other CLI (ADR-0004). Beside it the CLI writes the app configuration file, `.prisma-composer/app-configuration-<pid>-<uuid>.json`, mode 0600, and deletes it when the run ends. It holds what Load read from the app configuration and the first process's node addresses; it is not `prisma.config.ts`. The CLI passes its path in `PRISMA_COMPOSER_APP_CONFIGURATION_FILE`. The second process reads it, runs no adapter, and refuses a graph whose node addresses differ from the first process's. It never receives the app secrets. The CLI refuses to write the file when any value in it contains a known secret (`CONFIG.SECRET_AS_PLAIN_VALUE`). It also deletes the file when its process exits. At the start of each `deploy` and `destroy` it deletes the app configuration files an earlier run left in `.prisma-composer/`, when the process id in the file name is not running and the file is older than ten minutes. The CLI engine gives the child the terminal, so its output is not redacted; Composer redacts its failure cause. `deploy` writes no app configuration file: it has no second process, and `lower()` receives what Load read as a value.
`destroy` writes the pipeline's results as a runnable stack module at `.prisma-composer/alchemy.run.ts` and drives the `alchemy` CLI against it (ADR-0007), started as § Runtime describes, carrying every resolved container across to the child as one environment variable per extension — content the CLI writes but never reads (ADR-0038). `alchemy destroy` re-imports and re-evaluates the same stack, so its target reconstruction needs the containers. The generated file and Alchemy's state live in the process's working directory — tool state lives where you run the tool, like any other CLI (ADR-0004). Beside it the CLI writes the app configuration file, `.prisma-composer/app-configuration-<pid>-<uuid>.json`, mode 0600, and deletes it when the run ends. It holds what Load read from the app configuration and the first process's node addresses; it is not `prisma.config.ts`. The CLI passes its path in `PRISMA_COMPOSER_APP_CONFIGURATION_FILE`. The second process reads it, runs no adapter, and refuses a graph whose node addresses differ from the first process's. It never receives the app secrets. The CLI refuses to write the file when any value in it contains a known secret (`CONFIG.SECRET_AS_PLAIN_VALUE`). It also deletes the file when its process exits. At the start of each `deploy`, `destroy` and `prisma dev` it deletes the app configuration files an earlier run left in `.prisma-composer/`, when the process id in the file name is not running and the file is older than ten minutes. The child's output is piped through Composer's stream redactor, and Composer redacts its failure cause. `deploy` and `prisma dev` write no app configuration file: they have no second process, and `lower()` receives what Load read as a value.

The pass that assembles a service is the same pass that lowers it, so the
correlation between services and their built bundles never exists as
Expand All @@ -217,7 +217,7 @@ inputs carry the same `environment`.
([ADR-0038](../90-decisions/ADR-0038-containers-are-an-extension-descriptor.md)).
The CLI resolves every configured extension's container before Alchemy runs
(pipeline step 8) and holds each result as an opaque value.
- **Containers reach the lowering as values.** `deploy` hands each resolved container to `lower()` directly. For `destroy` and `prisma dev`, which start an Alchemy child, each container crosses to the child as one environment variable, named from the extension's id and holding whatever that extension's own `serialize()` produced; the child reads it back and calls the same extension's `deserialize`. The transport is content-blind: the CLI never reads the value, and an extension never touches `process.env` directly — it receives its container as a parameter everywhere. Preflight payloads travel the same two ways and reach the lowering as `LowerContext.preflight`.
- **Containers reach the lowering as values.** `deploy` and `prisma dev` hand each resolved container to `lower()` directly. For `destroy`, which starts an Alchemy child, each container crosses to the child as one environment variable, named from the extension's id and holding whatever that extension's own `serialize()` produced; the child reads it back and calls the same extension's `deserialize`. The transport is content-blind: the CLI never reads the value, and an extension never touches `process.env` directly — it receives its container as a parameter everywhere. Preflight payloads travel the same two ways and reach the lowering as `LowerContext.preflight`.
- **State rides the container.** `PrismaAppConfig.state` names its owning
extension; core hands that extension's own resolved container to
`state.create()`, so the state layer is built from it rather than from the
Expand Down Expand Up @@ -301,7 +301,7 @@ them:
| A config adapter throws or returns something other than an object (`CONFIG.ADAPTER_FAILED`, `CONFIG.ADAPTER_INVALID`) | which list and position, and the fix |
| Alchemy's plan or apply failed (`DEPLOY.ENGINE_FAILED`) | the errors Alchemy failed with |
| Ctrl-C during the plan or apply (`DEPLOY.INTERRUPTED`) | to deploy again; the lease is already released |
| `destroy` or `prisma dev`: the app configuration file would hold a known secret (`CONFIG.SECRET_AS_PLAIN_VALUE`) | which root input field or configuration key holds a known secret, and the secrets key whose value matched, never the value; a secret of eight characters or more counts anywhere in a value, a shorter one only as the whole value. The fix: read the secret with `envSecret()`, or give the plain value text that does not contain it |
| `destroy`: the app configuration file would hold a known secret (`CONFIG.SECRET_AS_PLAIN_VALUE`) | which root input field or configuration key holds a known secret, and the secrets key whose value matched, never the value; a secret of eight characters or more counts anywhere in a value, a shorter one only as the whole value. The fix: read the secret with `envSecret()`, or give the plain value text that does not contain it |

## Out of scope (designed around)

Expand Down
Loading
Loading