Skip to content

prisma dev reads secrets from the environment and never invents them - #382

Open
wmadden-electric wants to merge 13 commits into
mainfrom
claude/dev-secrets-no-placeholders
Open

wmadden-electric wants to merge 13 commits into
mainfrom
claude/dev-secrets-no-placeholders

Conversation

@wmadden-electric

@wmadden-electric wmadden-electric commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

prisma dev no longer invents secrets. A secret now comes only from the environment of the current run: the shell, or a .env that prisma.config.ts loads. A required secret that is unset stops the run:

$ prisma dev module.ts
prisma dev cannot start: …
  - AUTH_SIGNING_SECRET  (required by service "auth.service": signingKey must be present (was missing))

Set each missing variable in either of these ways:
  - export it in the shell you run `prisma dev` from, or
  - load a .env file from prisma.config.ts: install the dotenv package and add import 'dotenv/config' at the top of prisma.config.ts.

An optional secret that is unset now reaches the service as absent, so a feature that depends on it stays off. A secret whose value changed since the last run restarts the services that read it.

Closes #379. Closes #380.

The problem

Until now, prisma dev gave any secret the shell left unset or empty a made-up value, local-placeholder-<16 hex>, and saved it in .prisma-composer/dev/secrets.json. A service could not tell the made-up value from a real key. prisma/asks declares its Fireworks key optional ('fireworksApiKey?': secretString()) and turns embeddings off without it. Locally it received the made-up key, turned embeddings on, and every search failed with a 401 from Fireworks. prisma/asks had to recognise the placeholder by its text (prisma/asks#233). Because the value was saved and a warm start does not redeploy, a real key set later never reached a running service (#380).

What changes

  1. No made-up values (target/src/local-target/preflight.ts). The placeholder and its warning are gone.
  2. Required or optional is decided by the service's own schema. ADR-0042 forbids Composer from looking inside a schema, so the dev preflight validates each service's input with every unset secret left out. If the schema accepts that, the secrets were optional. If not, prisma dev stops and names each secret whose absence alone fails validation, with the service and the schema's message.
  3. The engine leaves optional secrets out. The dev preflight sends the names of absent secrets to the deploy-engine process on the variable deploy's preflight already uses (PRISMA_COMPOSER_PREFLIGHT_<extension>), as dev: followed by JSON. The serializer drops an envSecret from the input document only when its name is on that list. Deploy never sends the list, so dev and deploy still share one lowering (ADR-0041). One decoder reads both payload kinds: target/src/control/preflight-payload.ts.
  4. Nothing is stored between runs. secretsStore is removed. The local Deployment provider reads current values from process.env and gives each service only the secrets and env-sourced params its own rows point at (local-target/src/compute.ts). A leftover secrets.json is deleted on start.
  5. A changed value redeploys the services that read it. Hosted deploys already add a <name>:updatedAt member to each Deployment's triggers for every variable it points at. Under prisma dev that member is now an HMAC-SHA256 of the current value, under a random key in .prisma-composer/dev/fingerprint.key (mode 0600). When the value changes, Alchemy sees the trigger change and updates that deployment; other services are left alone. The key exists because Alchemy writes trigger members to its state file as given, and a bare hash would let someone check guesses against it.
  6. Docs. New ADR-0058 amends ADR-0041's secrets rule and records Will's reasoning. local-dev.md, running-locally.md and the core-concepts skill describe the new behaviour.
  7. Examples. examples/auth, examples/email and examples/storefront-auth stopped under prisma dev without their secrets. Each now has a committed .env.example, dotenv as a dev dependency, and import 'dotenv/config' in its prisma.config.ts.

Breaking for local dev

  • An app whose required secret is unset locally used to start with a made-up value. It now stops with an error naming the variable. Fix: set it in the shell, or load a .env from prisma.config.ts.
  • An optional secret that is unset now arrives absent instead of as a made-up value.
  • A service now receives only the secrets and env-sourced params its own input and params point at. Code that read another service's variable directly from process.env now gets nothing.
  • prisma deploy also evaluates prisma.config.ts. A .env loaded there is in the deploy's environment, and deploy uploads any secret the target stage does not have yet. Keep local-only values out of it before deploying, or deploy from a shell where it is not loaded. The guide, ADR-0058 and each example's .env.example say so.

Deploy behaviour does not change.

Tests

  • local-target-preflight.test.ts is rewritten: required unset or empty stops dev with the variable and the service; optional unset or empty is absent from the document; a secret shared by two services; the payload never carries a value; a damaged key file gives the path and the fix; two runs creating the key at once agree.
  • input-serializer.test.ts and preflight-payload.test.ts: deploy documents and deploy payload decoding are unchanged.
  • compute-materialize-env.test.ts and dev-pointed-variables.test.ts: services get current values, only their own, and the copied pointer formats match the serializer's.
  • operations.test.ts: prisma dev carries the payload to the engine and blanks a stale one.
  • test/integration/test/local-dev-criteria-4-5.integration.ts, criterion 5 rewritten end to end: required unset and empty stop dev; a value from the fixture's .env reaches the service with nothing in the shell; an optional secret arrives absent; after a warm start with a changed value only the service that reads it is redeployed and sees the new value; a leftover secrets.json is deleted.
  • Manual run on examples/storefront-auth, reading each service process's environment: unset stops with the error above; value A reaches auth.service and not the storefront; a warm start with value B gives auth.service value B; a value only in .env reaches the service.

Follow-up work

Recorded in .drive/deferred.md. The main item: a deploy still fails when an optional secret is missing on the platform. ADR-0042 says that case should deploy with the field left out.

Alternatives considered

  • Keep placeholders only for required secrets (prisma dev replaces a secret set to an empty string with a placeholder #379's first proposal). Rejected: a made-up credential still turns features on that then fail at the external service.
  • Read optionality from the schema. Rejected by ADR-0042, which allows only validate.
  • A separate transport variable for dev. Rejected: one variable per extension keeps core unaware of the payload's contents; the dev: prefix keeps deploy's payload shape unchanged.
  • Pass current values on every warm start instead of redeploying. Rejected: it needs a change to the emulator's resume API and restarts every service every time.
  • Composer loads .env itself under prisma dev only. This would keep local values out of prisma deploy. Rejected because Will chose the ORM's model, where the user owns their environment and prisma.config.ts loads it.

Agent: schubert-65

🤖 Generated with Claude Code

wmadden-electric and others added 11 commits October 23, 2026 12:09
…vents them

The dev preflight no longer mints placeholder secrets or stores values in .prisma-composer/dev/secrets.json. For each service it validates the input with every unset or empty secret left out. If the schema rejects that, dev stops with an error naming each missing variable, the service, the schema message, and how to set it (shell export or a .env loaded from prisma.config.ts). Optional unset secrets are left out of the input document.

The preflight returns a payload that the CLI carries to the alchemy process on the preflight transport: the absent secret names, and a SHA-256 hash of each pointed variable value. The serializer leaves a secret out only when that payload names it; deploys send none, so their documents are unchanged.

The local Deployment provider reads current values from process.env and gives each service only the variables its own rows point at. A leftover secrets.json is deleted when prisma dev starts.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A validator that throws (for example an async schema) again fails deploy with an error that names the service. The dev preflight reports an input that is invalid for reasons other than missing secrets next to the other problems, in dev wording, instead of throwing the deploy error at once. The env-param error now gives both ways to set a value, and the ways to set a value are printed once.

Adds a test that the local Deployment provider reads the same pointer formats the serializer writes, and a test that the compute descriptor leaves out a secret the dev payload names absent. The compute descriptor passes process.env to the serializer explicitly.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under prisma dev, the <name>:updatedAt trigger member of each Deployment now holds the SHA-256 fingerprint the dev preflight carries for that variable, so it changes exactly when the value changes. The local Deployment provider diff returns nothing when the artifact is unchanged, so the engine compares the props, sees the changed trigger, and reconciles; the provider then gives the emulator the new environment. Deploy still reads platform timestamps.

The local-dev fixture gains an optional secret and loads a .env from its own prisma.config.ts. The criteria 4 and 5 integration test is rewritten: a required secret that is unset or empty stops dev, an unset env-sourced param stops dev, a value loaded by prisma.config.ts reaches the service, an unset or empty optional secret arrives absent, and a changed value reaches the service on a warm start. The local-dev integration test now hands the alchemy process the preflight payload, as the CLI does.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alchemy writes trigger members to its state as given, so a bare SHA-256 of a secret value ended up in the project, where a guessable value could be recovered. The dev preflight now uses HMAC-SHA256 with a random 32-byte key that it creates once in .prisma-composer/dev/fingerprint.key (mode 0600) and reuses. Losing the key causes one redeploy; --fresh deletes it.

The criteria 4 and 5 integration test now writes a leftover secrets.json before a dev run and checks that the run deletes it. Comments in the compute descriptor no longer claim that Alchemy stores only a salted hash of the triggers.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…0056)

ADR-0056 amends ADR-0041: prisma dev takes secrets only from the shell or a .env loaded by prisma.config.ts, a required unset secret is an error, an optional one is absent, nothing is stored, and a changed value redeploys through an HMAC fingerprint in the trigger members. Deploy is unchanged. The ADR index, ADR-0041, local-dev.md, running-locally.md and the core-concepts skill no longer describe placeholder secrets.

The preflight error now leads with import dotenv/config and says process.loadEnvFile works only under Node, because Bun does not implement it.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he docs

examples/auth, examples/email and examples/storefront-auth each stop under prisma dev now that a required secret is no longer made up: auth and email need EMAIL_DELIVERY_CREDENTIAL (plus the EMAIL_DELIVERY_MODE and EMAIL_FROM params they always needed), storefront-auth needs AUTH_SIGNING_SECRET. Each gets a committed .env.example with harmless local values, dotenv as a dev dependency, and import dotenv/config at the top of its prisma.config.ts. .env stays ignored by the root .gitignore.

The guide, the skill and the preflight error now say to install dotenv before importing it. ADR-0056, ADR-0041, the ADR index and the guide no longer claim that no value is stored: Composer never reads a value back from an earlier run, and ADR-0056 records that the compute emulator keeps a copy of each running service environment. The value-sourcing paragraph in local-dev.md is split and drops project shorthand. .drive/deferred.md no longer describes secrets.json as current and is committed with the entries for this slice.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l code review

The deploy and dev payloads are decoded in one place, control/preflight-payload.ts, which returns a tagged result. The lowering reads one preflight field on its options: pointerVersion (the platform updatedAt on deploy, an HMAC of the value under prisma dev) and absentSecrets as a plain set. The persisted <name>:updatedAt trigger key is unchanged, so hosted deployments do not redeploy. serializeInput takes an options object, so the compute descriptor no longer reads the environment.

The fingerprint key moves to dev-secrets.ts as readOrCreateFingerprintKey. It is linked into place, so two first runs agree on one key, and a damaged key stops dev with the file path and the fix. The dev error starts with "prisma dev cannot start". execute-dev blanks the transport variable of every configured extension, as deploy does. Comments no longer say SHA-256, and the core transport header says it carries either preflight.

Tests: the CLI carries the dev payload and blanks stale variables; a secret shared by two services; a damaged key; the deploy cases moved next to the serializer and the payload decoder; the criteria 4 and 5 integration test checks that a changed secret redeploys bkg and leaves web alone.
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ADR-0056

A .env loaded by prisma.config.ts is read by every prisma command. prisma deploy uploads any secret the target stage does not have yet from it, so running-locally.md, local-dev.md and ADR-0056 now say to keep local-only values out of it before deploying, or to deploy from a shell where it is not loaded.

ADR-0056 now states that env-sourced params get the same sourcing, that dev and deploy write different input documents for an unset optional secret until deploy gets the same fix, that the lowering takes two dev inputs (absent names and fingerprints), that the <name>:updatedAt key name is kept on purpose, and that a service now receives only the variables it points at. The guide says the same about the narrower environment. The ADR-0041 header line says which of its passages were updated, and the ADR index line matches.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ockfile

Each .env.example now says that prisma deploy from the same directory reads it and uploads any secret the target stage does not have yet. storefront-auth leaves AUTH_SIGNING_SECRET empty, so a developer must choose one and the published value can never become a hosted signing key; prisma dev stops with an error naming it until then. The email examples keep EMAIL_DELIVERY_MODE=none and say that deploying with this .env loaded gives the stage a mode that sends no email.

The lockfile diff against main is now only the three dotenv entries; the unrelated alchemy peer reshuffles for examples/bucket and examples/cron are reverted. pnpm install --frozen-lockfile accepts it.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
serializeInput has its doc comment back. resolveInputBinding and inputValidationFailure take the same options object as serializeInput ({ env, absentSecrets }), with one place supplying the defaults.

Creating the fingerprint key falls back to writeFile with flag wx when the file system refuses hard links, and treats an existing file the same way. A new test races eight first runs and checks they all get one key.

ADR-0056 says how loading .env in Composer would compare on the deploy upload risk, and why it was still rejected. The deferred entry for the copied wire format names s3-protocol as the model.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@prisma-gizmo

prisma-gizmo Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

✅ Gizmo reviewed b9efe0a — posted 0 inline comment(s) this pass.

Open findings: none

Change walkthrough

prisma dev stops inventing secrets: a secret now comes only from the run's environment (shell or a .env that prisma.config.ts loads), a required unset secret stops dev with an error naming the variable and service, an optional one reaches the service as absent, and a changed value redeploys exactly the services that read it. Closes #379 and #380.

Dev preflight and serializer. Required-vs-optional is decided by validating each service's input with unset secrets left out (ADR-0042 compliant — the schema is never read). Absent names travel to the deploy-engine process on the existing PRISMA_COMPOSER_PREFLIGHT_<extension> transport with a dev: prefix, so one payload decoder serves both dev and deploy and deploy's payload shape is untouched. The serializer drops an envSecret leaf only when the payload names it absent; deploy behaviour is unchanged. The secrets.json store is gone — values are read fresh from process.env each run, and a leftover file is deleted on start. Changed values redeploy via the existing <name>:updatedAt trigger member, now an HMAC-SHA256 under a per-checkout random key (mode 0600) so Alchemy's state file can't be used to guess short secrets.

Examples and docs. examples/auth, examples/email and examples/storefront-auth get committed .env.example files, dotenv dev dependencies and import 'dotenv/config' in their prisma.config.ts. New ADR-0058 (renumbered from 0057, which main took for node addressing) records the decision; local-dev.md, running-locally.md and the core-concepts skill describe the new behaviour, including the caveat that prisma deploy also sees a config-loaded .env.

This pass's delta. The incremental diff is dominated by the rebase onto main's ADR-0056 dev-server work and ADR-0057 addressing work, and it all coheres with the secrets change: materializeEnv feeds dev servers the same pointed-at values from the current environment (with restart-on-change covered by the new serve tests), the pure stashRows refactor keeps boot and dev-server stashing on one path, and resource-ID/fromLegacyId migrations preserve existing deployed keys. Test suites were re-anchored to full addresses (localdevs4fixture.bkg), with per-address deployment-state assertions proving a secret rotation redeploys only the reader. The ADR renumbering left no stale references; integration and unit coverage now spans unset/empty required and optional secrets, shared secrets, warm-start rotation, and secrets.json cleanup.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: a6257114-430c-45b4-884e-984f7b50023e
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Main took ADR-0056 for the dev-server decision. The secrets decision becomes ADR-0057, and both amendments are kept on ADR-0041 and its index line.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pkg-pr-new

pkg-pr-new Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@prisma/composer@382
npm i https://pkg.pr.new/@prisma/composer-cli@382
npm i https://pkg.pr.new/@prisma/composer-prisma-cloud@382

commit: b9efe0a

@prisma-gizmo prisma-gizmo Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New findings: none · trace

@prisma-gizmo prisma-gizmo Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No Gizmo finding is open and the head commit has been reviewed. Approving.

Brings in dev servers (#381), full node addresses (#384, ADR-0057), the deploy entry in prisma.config.ts (#387) and prisma deploy list (#388). The secrets decision becomes ADR-0058, and every reference to it moves with it.

A dev server gets its environment from materializeEnv, so it receives the same per-service secret values from process.env, an unset optional secret is absent, and a changed value restarts it; the serve test now covers both. The docs that described placeholders for dev servers now describe reading secrets before the dev server starts. Tests and expected error text use full addresses.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@prisma-gizmo prisma-gizmo Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New findings: none · trace

@prisma-gizmo prisma-gizmo Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No Gizmo finding is open and the head commit has been reviewed. Approving.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

prisma dev does not redeploy a service when a secret it reads changes prisma dev replaces a secret set to an empty string with a placeholder

2 participants