---
title: 'Secrets Management'
description: 'How Cartwright stores and resolves secrets — Vercel env vars, DB-encrypted integration keys, and AUTH_SECRET rotation impact.'
canonical: 'https://cartwright.app/docs/configuration/secrets-management'
---

# Secrets Management (/docs/configuration/secrets-management)



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 [#secret-lookup-order]

```bash
# 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)
```

<Mermaid
  chart="`
flowchart LR
A[Request needs integration key] --> B{IntegrationSettings\nrow has value?}
B -->|yes| C[Decrypt AES-256-GCM\nwith AUTH_SECRET KEK]
C --> D[Use decrypted key]
B -->|no| E{Env var set?}
E -->|yes| F[Use env var value]
E -->|no| G[Feature disabled\nor graceful fallback]
`"
/>

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 [#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 [#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-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.

```bash
# 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 [#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.
