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:
| Variable | Scope | Notes |
|---|---|---|
AUTH_SECRET | Build + Runtime | KEK for all DB-stored secrets. Rotate with care — see below. |
TURSO_DATABASE_URL | Runtime | Prisma libSQL adapter connection string. |
TURSO_AUTH_TOKEN | Runtime | Turso auth token. Stripped of non-printable ASCII by lib/db.ts. |
NEXT_PUBLIC_APP_URL | Build + Runtime | Baked into static metadata at build. Must match canonical domain. |
CRON_SECRET | Runtime | Vercel Cron auth for /api/cron/reconcile-stripe. |
UNSPLASH_ACCESS_KEY | Runtime | Admin 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
nulland 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:
- 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.
- All DB-stored integration keys become unreadable until they are re-saved. After rotating, go to
/admin/integrationsand 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 --prodAfter 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_SECRETseparately 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
IntegrationSettingsrows. They are still encrypted and require the sameAUTH_SECRETthat was in force when they were written.