EmDash uses a small set of secrets across previews, comments, authentication, storage, and plugins. This page is the complete inventory: where each secret comes from, where it is stored, how to rotate it, and what breaks if it is lost.
Overview
| Secret | Source | Stored in | Lost key impact |
|---|---|---|---|
EMDASH_ENCRYPTION_KEY | Operator (emdash secrets generate) | Environment / Worker secret only | Encrypted plugin secrets become unrecoverable (once encryption at rest ships) |
| Preview secret | Auto-generated (env override) | options table (emdash:preview_secret) | Outstanding preview links stop working; new ones are fine |
| IP salt | Auto-generated (env override) | options table (emdash:ip_salt) | Past comment rate-limit continuity resets |
| Session & API tokens | Generated per session/token | Session store / database (hashes only) | Nothing — plaintext is never stored |
| OAuth provider credentials | You (Google/GitHub console) | Environment | Sign-in via that provider stops until replaced |
| Turnstile secret | You (Cloudflare dashboard) | Environment | Comment CAPTCHA verification fails |
| S3 credentials | You (storage provider) | Environment or config | Media upload/download fails until replaced |
| Plugin secrets | You (admin settings UI) | Database (plugin settings / storage) | Re-enter in the admin |
| CLI credentials | emdash login / emdash plugin publish device flows | ~/.config/emdash/auth.json (mode 0600) | Run the device flow again |
| Registry CLI credentials | emdash-plugin atproto OAuth | ~/.emdash/oauth/, ~/.emdash/credentials.json (mode 0600) | Log in again; identity lives at your PDS |
The encryption key
EMDASH_ENCRYPTION_KEY is the site’s key for encrypting plugin secrets at rest. It is operator-provided and never stored in the database — the database only ever holds ciphertext, so a leaked database backup does not expose the key.
Generate one and set it as an environment variable (or Worker secret):
npx emdash secrets generate
# emdash_enc_v1_<43 base64url chars>
# Cloudflare:
wrangler secret put EMDASH_ENCRYPTION_KEY
The format is emdash_enc_v1_ followed by 32 random bytes as unpadded base64url. The key is validated at runtime startup; a malformed value logs an operator-facing error without taking down request paths.
Rotation
The variable accepts a comma-separated list of keys. The first entry is the primary and is used for new writes; all entries are tried for decryption. Every encrypted value is tagged with an 8-character key fingerprint (the kid, printable via emdash secrets fingerprint <key>), so the runtime picks the right key automatically.
To rotate: generate a new key, prepend it to the list (EMDASH_ENCRYPTION_KEY="new,old"), redeploy, and drop the old key once existing values have been re-encrypted.
Generated site secrets
Two secrets are generated automatically on first use and persisted in the options table, so they are stable across requests, deployments, and isolates. Generation is atomic — concurrent cold starts converge on one value.
Preview secret
Signs preview URLs (HMAC). Stored as emdash:preview_secret; 32 random bytes, base64url.
- Override: set
EMDASH_PREVIEW_SECRET(legacy alias:PREVIEW_SECRET) if you need the same secret across multiple processes or want to pin it for audit reasons. The environment always wins over the stored value. - Rotation: delete the
emdash:preview_secretrow (or change the env var) and redeploy. Impact: previously issued preview links stop validating. Nothing else breaks — a fresh secret is generated (or read from the env) on the next preview request. - If lost: nothing is unrecoverable. Preview links are short-lived by design.
See the preview guide for how preview URLs are built and verified.
IP salt
Salts the SHA-256 hash of commenter IP addresses (ip_hash on comments) used for comment rate limiting. Stored as emdash:ip_salt. Site-specific, so hashes are not correlatable across EmDash installs.
- Override: set
EMDASH_IP_SALT. For backward compatibility,EMDASH_AUTH_SECRET/AUTH_SECRETare also consulted — installs that historically derived the salt from those keep stable hashes. - Rotation: change the env var or delete the
emdash:ip_saltrow. Impact: new comment submissions hash to different values, so rate-limit counting restarts for everyone. Existing comments and their stored hashes are untouched. - If lost: no data loss. Only rate-limit continuity resets.
Session and API tokens
- Sessions use Astro’s session store (Workers KV on Cloudflare, filesystem on Node). The cookie carries an opaque session ID; there is no signing secret to manage. Sign out to end a session, or clear the session store (e.g. the KV namespace) to force everyone to sign in again.
- API tokens (
ec_pat_,ec_oat_,ec_ort_prefixes) are opaque 256-bit random values; only their SHA-256 hash is stored. The plaintext is shown once at creation. Rotate by revoking and re-creating in the admin. - Invite, magic-link, and recovery tokens are single-purpose, stored as SHA-256 hashes in
auth_tokens, and time-limited (invites 7 days, magic links 15 minutes).
There is nothing to back up or rotate proactively: a database leak exposes only hashes, and every token can be revoked or reissued from the admin.
User-provided service credentials
Credentials for external services are read from the environment and never written to the database. Rotate them at the provider, update the variable, redeploy.
| Service | Variables |
|---|---|
| Google sign-in | EMDASH_OAUTH_GOOGLE_CLIENT_ID, EMDASH_OAUTH_GOOGLE_CLIENT_SECRET (or unprefixed aliases) |
| GitHub sign-in | EMDASH_OAUTH_GITHUB_CLIENT_ID, EMDASH_OAUTH_GITHUB_CLIENT_SECRET (or unprefixed aliases) |
| Marketplace publishing (CI) | EMDASH_MARKETPLACE_TOKEN |
| Turnstile (comments) | EMDASH_TURNSTILE_SECRET_KEY (or TURNSTILE_SECRET_KEY) |
| S3-compatible storage | S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_ENDPOINT, S3_BUCKET, S3_REGION |
On Cloudflare, set these with wrangler secret put; locally, put them in .env. R2 via binding needs no credentials at all — access is granted by the binding in wrangler.jsonc, which is the recommended setup on Workers. See storage options.
Plugin secrets
Settings a plugin declares with type: "secret" (API keys for email providers, form CAPTCHAs, etc.) are entered in the admin UI and stored in the database — in the options table under plugin:<id>:settings:<key>, or in the plugin’s key-value storage. Whether a stored secret is echoed back to the admin UI is up to the plugin; well-behaved plugins return only a “value is set” flag instead of the secret itself (the bundled forms plugin does this).
- Rotation: rotate the key at the provider and paste the new value into the plugin’s settings page. Takes effect immediately.
- If lost: re-enter the value in the admin. Nothing else depends on it.
CLI credentials
The emdash CLI holds two kinds of credentials, both in ~/.config/emdash/auth.json (respecting XDG_CONFIG_HOME), created with owner-only permissions (0600):
- Site tokens —
emdash loginauthenticates against your EmDash instance via an OAuth device flow and stores the resulting token keyed by instance URL.emdash logoutremoves it; per invocation,--tokenorEMDASH_TOKENoverrides the stored token. - Marketplace tokens —
emdash plugin publishauthenticates to the EmDash Marketplace via a GitHub device flow and stores the resulting JWT keyed bymarketplace:<origin>. For CI publishing, setEMDASH_MARKETPLACE_TOKENinstead — it takes priority over the stored credential.
Losing the file is harmless: run emdash login (or emdash plugin publish, which re-runs the device flow) again.
Plugin registry CLI credentials
The separate emdash-plugin CLI (package @emdash-cms/plugin-cli) targets the experimental AT Protocol registry. Publishing there is tied to your AT Protocol identity (your publisher DID) — the site itself holds no publishing credentials, and installs verify artifacts against checksums from release records attributed to that DID.
- It authenticates via atproto OAuth. The OAuth session/state blobs live in
~/.emdash/oauth/, and the publisher identity (DID, handle, PDS) is cached in~/.emdash/credentials.json; both are written with owner-only permissions. - In CI, provide identity via
EMDASH_PUBLISHER_DID,EMDASH_PUBLISHER_HANDLE, andEMDASH_PUBLISHER_PDS;EMDASH_REGISTRY_URLoverrides the registry host. Automatedpublishfrom CI still needs the OAuth session files in~/.emdash/oauth/on the runner — the env vars alone do not carry the OAuth session. - Rotating or revoking publishing access happens at your AT Protocol account (e.g. app passwords), not in EmDash. See Atmosphere auth.
Rotation quick reference
| I want to… | Do this |
|---|---|
| Rotate the encryption key | Prepend a new key: EMDASH_ENCRYPTION_KEY="new,old", redeploy, drop old later |
| Invalidate all preview links | Delete the emdash:preview_secret option row (or change the env override) |
| Reset comment rate-limit hashing | Change EMDASH_IP_SALT (or delete the emdash:ip_salt option row) |
| Revoke a leaked API token | Admin → Users → API tokens → revoke, then create a replacement |
| Kill all sessions | Clear the session store (Workers KV namespace / session directory) |
| Replace a provider credential | Rotate at the provider, update the env var, redeploy |
| Replace a plugin API key | Rotate at the provider, re-enter in the plugin’s admin settings |