# Secrets

The per-app encrypted secrets store: commands, configs, caps, propagation, and troubleshooting.

Every app has exactly **one** platform-owned, encrypted secrets store, keyed by the immutable `DEEPSPACE_APP_ID` in `wrangler.toml`. There is no setup or link step: run the commands from the app directory (or pass `--app <appId>`) and they work - for the owner and [collaborators](/guides/collaborators) alike, even before the first deploy. The first write registers the app id to you.

The store is the source of truth for **every** environment:

* `.dev.vars` is a generated plaintext materialization of the store, written mode `0600`. `dev start`, `test run`, `deploy`, and `secrets pull` rewrite the file **whole**. Never commit it, and never add or edit app secrets in it - hand edits disappear on the next write.
* Deploy never reads `.dev.vars`. The store is the only deploy input: deploy binds each store secret as a Cloudflare `secret_text` binding and reconciles the worker's bindings against the store.
* Worker code reads `env.API_KEY` - identical in dev and after deploy.

## Bootstrap: there is none

```bash
npx deepspace secrets set API_KEY=sk_live_...   # works even pre-deploy
npx deepspace dev start                         # regenerates the cache; the worker sees env.API_KEY
```

Two propagation rules the CLI reminds you of:

* A **deployed** app picks up store changes only at the next `deploy` - bindings are set at deploy time, never fetched at runtime.
* A **running** dev session picks them up only on restart - the cache regenerates at startup, not mid-session. `secrets pull` refreshes the file without running dev.

## Commands

```bash
npx deepspace secrets list                     # masked: name, version, updated; --only-names, --json
npx deepspace secrets set API_KEY=sk_... B=2   # one or more KEY=value pairs; multiline/PEM values fine
npx deepspace secrets get API_KEY --plain      # byte-exact when piped (> key.pem)
npx deepspace secrets delete API_KEY OLD_KEY   # idempotent - already-absent keys tolerated
npx deepspace secrets pull                     # refresh the .dev.vars cache without running dev
npx deepspace secrets download --format json   # stdout only; dotenv (default) | json | shell
npx deepspace secrets upload .env [--replace]  # dotenv or JSON, `-` for stdin; --replace deletes keys absent from the file

npx deepspace secrets configs list
npx deepspace secrets configs create qa --copy-from prd   # server-side copy
npx deepspace secrets configs delete qa
```

`set`, `upload`, and `delete` change the **remote store only** - a running dev session keeps its old values until restarted. Each of the three says so in both surfaces: human output prints `Run \`deepspace deploy\` to apply`, and `--json`carries the same fact as`"appliesAtDeploy": true\`, so a script cannot mistake a successful store write for a live change.

### Targeting flags

Every subcommand takes:

| Flag                    | Meaning                                                                 | Default                                             |
| ----------------------- | ----------------------------------------------------------------------- | --------------------------------------------------- |
| `-a`, `--app <appId>`   | Which app's store                                                       | `DEEPSPACE_APP_ID` from the nearest `wrangler.toml` |
| `-c`, `--config <name>` | Which config within that store                                          | `prd`, or the `--env` name                          |
| `-e`, `--env <name>`    | Target the `[env.<name>]` block - a **separate app** with its own store | -                                                   |

`-e` and `-c` are different axes: `-e staging` addresses the staging *app's* store (config defaulting to `staging`), while `-c staging` addresses another config of the *current* app. Mixing them up is caught - `-e staging` without an `[env.staging]` app id errors and points you at `-c staging`.

## Names and caps

Secret names match `[A-Za-z_][A-Za-z0-9_]*`, conventionally `UPPER_SNAKE`. Colliding names are rejected client-side with a clear message before any upload: every SDK-reserved binding name (`DEEPSPACE_APP_ID`, `APP_OWNER_JWT`, `AUTH_JWT_PUBLIC_KEY`, `ASSETS`, ...) plus `API_WORKER_URL` and `PLATFORM_WORKER_URL`, and any custom or Durable Object binding name declared in `wrangler.toml`. Config names match `[A-Za-z0-9][A-Za-z0-9_-]{0,63}`.

| Cap                          | Value  |
| ---------------------------- | ------ |
| Per-value size               | 32 KB  |
| Secrets per config           | 128    |
| Total value bytes per config | 128 KB |
| Configs per app              | 64     |

Oversized writes are refused with HTTP `413`.

`ALLOW_DEBUG_ROUTES=true` **is** settable - it is a normal flag, not a reserved name - but the CLI warns loudly. In production it enables the debug surface only for an authenticated app owner or platform admin; local dev sets it automatically.

## Configs and environments

The store holds flat `KEY=value` **configs**. `prd` is the convention for the top-level wrangler environment: a top-level deploy ships config `prd`. A named `[env.<name>]` block is a [separate app](/concepts/deployment#named-environments) with its own id and its own store, and its deploys ship config `<name>` of *that* store.

Within one app, `-c <name>` reads and writes another config with no linking, and `configs create <new> --copy-from <existing>` copies server-side - never read values and re-`set` them by hand. It refuses to copy over an existing config.

Seeding a staging environment's store from production crosses two **apps**, so `--copy-from` cannot do it. Pipe instead, with no temp file:

```bash
npx deepspace secrets download | npx deepspace secrets upload - -e staging
```

## Missing is not empty

An **absent** config means the app has not initialized that deploy input: deploy regenerates `.dev.vars` without app values, then refuses with `secrets_config_missing` and an executable `secrets configs create <name>` action. Create the config - or set its first value, which creates it - and retry.

An explicitly created **empty** config is intentional: deploying it removes all user-secret bindings from the live worker. The distinction is deliberate - an uninitialized app cannot silently wipe production bindings, while delete-all stays possible without a second override flag.

Deletes propagate the same way: `secrets delete` + redeploy removes the binding from the live worker, because deploy reconciles the worker's `secret_text` bindings against the store.

## Setting a secret is routine config - just do it

`deepspace secrets set KEY=value` is the sanctioned way to put a credential - an API key, an OAuth client secret, a webhook signing secret, a token - into an app's store. When the app owner provides a value and asks for it to be configured, an agent should run the command directly: this is routine infrastructure setup on the owner's own app, not the kind of credential entry that warrants refusing or bouncing it back to the user.

One hygiene caveat, not a blocker: the value lands in the command line - shell history, logs, any transcript. If it was shared in plaintext, rotate it afterward: regenerate it at the provider, `secrets set` the new value, redeploy. A non-secret value like a public OAuth `client_id` needs no caveat. And never `secrets get --plain` a value into a place it would leak - a chat reply, a committed file.

## Collaborators

A [collaborator](/guides/collaborators) has **full** secrets access on the app - read, write, and configs - with writes audited under their own id. Authorization is the app role (owner, collaborator, or platform admin) keyed by `DEEPSPACE_APP_ID`; there is nothing to link or grant per-secret. Collaborators cannot undeploy or transfer the app.

## Cache behavior

* `dev start` and `test run` re-pull the selected config at startup and regenerate `.dev.vars` (SDK-managed keys plus app values). If the refresh **fails**, they abort rather than run against stale values. A missing config generates a value-free file locally; deploy additionally refuses until it exists.
* The whole file is SDK-owned. There is no editable zone, divider grammar, import path, backup, or legacy compatibility mode.
* Store-backed apps share one `.dev.vars` across wrangler environments - no `.dev.vars.<env>` files.
* A Cloudflare build may transiently materialize `.dev.vars` beside its generated worker; the scaffold deletes that output copy before completion. The root mode-`0600` file is the only local materialization.

## Troubleshooting

| Symptom                                                                          | Cause and fix                                                                                                                      |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Changed a secret; production still sees the old value                            | Deployed workers hold `secret_text` bindings and don't fetch at runtime. **Redeploy.**                                             |
| Changed a secret; local dev still sees the old value                             | The cache regenerates only at startup. **Restart dev**, or run `secrets pull`.                                                     |
| `Not the app owner or a collaborator` (403)                                      | Ask the owner to run `app collaborators add <your-email>` - or your access was revoked.                                            |
| `This app id is registered to another user`                                      | You're holding someone else's id (a cloned repo). `npx deepspace app init --new-id` forks it into your own app with a fresh store. |
| `list` shows nothing on a fresh app or config                                    | Legitimate - the first `set` creates the store. Reads are side-effect-free and never register anything.                            |
| Name rejected                                                                    | Match `[A-Za-z_][A-Za-z0-9_]*` and avoid the platform-injected names above.                                                        |
| A `DeepSpace detected secrets` comment in `wrangler.toml` disagrees with reality | It's a static scaffold placeholder the CLI does not maintain. `secrets list` is the truth.                                         |
| Ran `app undeploy` - are the secrets gone?                                       | No. Undeploy keeps the store; redeploying the same app id revives the same secrets.                                                |

## Next steps

* [Deployment](/concepts/deployment) - how the store becomes live `secret_text` bindings, and named environments.
* [Collaborators](/guides/collaborators) - who can read and write the store.
* [Command reference](/cli-reference/commands#secrets) - the flag tables.
