cartwright
Configuration

Secrets Management

How Cartwright stores and resolves secrets — Vercel env vars, DB-encrypted integration keys, and AUTH_SECRET rotation impact.

Cartwright secrets live in two places: Vercel environment variables and an encrypted database row. Understanding which is which matters for key rotation, backup, and incident response.

Secret lookup order

# For an integration key (Stripe, Resend, Anthropic, Gemini)
# 1. DB: IntegrationSettings row (AES-256-GCM, KEK = SHA-256(AUTH_SECRET))
# 2. Env: STRIPE_SECRET_KEY / RESEND_API_KEY / ANTHROPIC_API_KEY etc.
# 3. Disabled: feature degrades gracefully (mock mode / console log / 503)

The DB value wins when both are present. This means you can rotate a key via /admin/integrations without touching Vercel env vars or redeploying.

Vercel environment variables

Some secrets are env-only and have no DB storage:

VariableScopeNotes
AUTH_SECRETBuild + RuntimeKEK for all DB-stored secrets. Rotate with care — see below.
TURSO_DATABASE_URLRuntimePrisma libSQL adapter connection string.
TURSO_AUTH_TOKENRuntimeTurso auth token. Stripped of non-printable ASCII by lib/db.ts.
NEXT_PUBLIC_APP_URLBuild + RuntimeBaked into static metadata at build. Must match canonical domain.
CRON_SECRETRuntimeVercel Cron auth for /api/cron/reconcile-stripe.
UNSPLASH_ACCESS_KEYRuntimeAdmin AI image search. No DB storage.

Sentry variables (SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT) are build-time only — used by the Sentry Vercel integration to upload source maps. They do not need to be set in Runtime scope.

DB-stored integration keys

Keys stored via /admin/integrations are encrypted with AES-256-GCM before writing to the IntegrationSettings table. The encryption implementation is in lib/secret-encryption.ts:

  • The Key Encryption Key (KEK) is SHA-256(AUTH_SECRET) — a deterministic 256-bit key derived from the auth secret.
  • Each encrypted payload carries its own random 96-bit IV and 128-bit GCM auth tag.
  • Format: base64( IV(12) || authTag(16) || ciphertext ).
  • Decryption failure (wrong KEK, corrupted ciphertext) returns null and falls through to the env var fallback. Sentry captures the exception so silent fallback to a wrong key is visible in monitoring.

Legacy plaintext keys (identifiable by sk-ant-, sk_live_, AIza prefixes) are returned as-is during a transition period. The next time the admin saves the key via the UI, it gets encrypted.

Integration keys are cached in-process for 30 seconds. A key rotation via /admin/integrations takes effect on the next request after the cache expires, without a redeploy.

AUTH_SECRET rotation

AUTH_SECRET serves two purposes: it signs JWT session tokens, and it is the KEK for all IntegrationSettings secrets.

Rotating AUTH_SECRET has two consequences:

  1. All active user sessions are invalidated immediately. Every logged-in admin and customer is signed out on the next request. There is no grace period.
  2. All DB-stored integration keys become unreadable until they are re-saved. After rotating, go to /admin/integrations and re-enter each key. The UI will re-encrypt them with the new KEK.
# Generate a new AUTH_SECRET
npx auth secret

# Update in Vercel
vercel env rm AUTH_SECRET production
vercel env add AUTH_SECRET production
# enter new value

# Redeploy to apply
vercel --prod

After the redeploy, re-save every integration key in /admin/integrations.

Backup and restore

IntegrationSettings is a single-row table (id = 1). Encrypted values are opaque without AUTH_SECRET. When taking a database backup:

  • Back up AUTH_SECRET separately from the database dump.
  • Store them in different locations. A dump is useless for secret recovery without the KEK, and a KEK is dangerous without the dump.
  • Turso's point-in-time restore snapshots include IntegrationSettings rows. They are still encrypted and require the same AUTH_SECRET that was in force when they were written.

On this page