Security
Secrets management
Control 6.7.1 and the OWASP Secrets Management Cheat Sheet — dedicated store, no hardcoded or Git-committed secrets, least privilege, rotation and revocation, access monitoring, and immediate replacement of a compromised secret.
Last updated: 28 August 2026
1. Access-control policy
- Server secrets live only as Vercel project environment variables for this project, scoped to Production and Preview. They are marked Sensitive. Values are hidden in the dashboard after save (lock / Sensitive). They are not
NEXT_PUBLIC_and are never bundled to the browser. - Only Vercel project members with environment-variable permission can view or edit them. There is no in-app admin console that displays secret values.
- Application source does not contain live secrets. Configuration is injected at deploy by the Vercel runtime.
- Per-user Google refresh tokens are readable only by the server process that holds
TOKEN_ENCRYPTION_KEY. End users, browsers, and client JavaScript cannot request the ciphertext or the key. - Decryption happens only on the server, only to call the Gmail API or to revoke the token when a mailbox or account is deleted.
- Application logs must not record secret values, session tokens, card data, or raw refresh tokens (see also 6.5.1).
2. Cryptographic protection
Platform secrets (API keys, AUTH_SECRET, TOKEN_ENCRYPTION_KEY, database URL, billing keys) are not copied into the application database. Vercel stores project environment variables encrypted at rest and injects them into the serverless runtime. That is the storage mechanism for those secrets.
Application secrets that must persist (Google refresh tokens) are encrypted in the application with AES-256-GCM before they are written to Postgres:
- Algorithm: AES-256-GCM (Node.js
crypto.createCipheriv) - Key: 32-byte value from
TOKEN_ENCRYPTION_KEY(64 hex chars) - IV: 12 random bytes per encryption
- Stored form:
iv.ciphertext.authTag(base64 parts) - Tampered ciphertext fails closed (auth-tag verification throws; no plaintext)
Session tokens are encrypted JWEs using a key derived from AUTH_SECRET (alg=dir, enc=A128CBC-HS256). Short-lived Google access tokens are never stored.
3. Secret inventory
| Secret | Class | Store | Protection |
|---|---|---|---|
| AUTH_SECRET | Platform secret | Vercel env (Sensitive) | Used to encrypt/sign session JWEs (dir / A128CBC-HS256). Never written to the database or the browser. |
| TOKEN_ENCRYPTION_KEY | Platform secret | Vercel env (Sensitive) | 32-byte AES-256 key (hex). Unlocks stored Google refresh tokens only. Never logged. |
| GOOGLE_CLIENT_SECRET | Platform secret | Vercel env (Sensitive) | OAuth client secret. Server runtime only; used to exchange authorization codes and refresh access tokens. |
| OPENAI_API_KEY | Platform secret | Vercel env (Sensitive) | Provider API key. Read from process.env on the server. Never sent to the browser or stored in Postgres. |
| DATABASE_URL | Platform secret | Vercel env (Sensitive) | Postgres connection string (Supabase). Server-only. TLS to the database. |
| RESEND_API_KEY, Inngest keys, Stripe / Paddle / cron / Pub/Sub secrets | Platform secret | Vercel env (Sensitive) | Same control: env-only, not NEXT_PUBLIC_, not in git, not in client bundles. |
| Google refresh token (per mailbox) | Application secret | Postgres column refresh_token_enc | AES-256-GCM before INSERT/UPDATE. Format iv.ciphertext.authTag. Decrypted in memory only for Gmail API or revoke. |
| Google access token | Ephemeral | Not stored | Minted in process memory by the Google client from the refresh token. Never persisted, never sent to the browser. |
| Session cookie | Session secret | HttpOnly Secure cookie | Encrypted JWE bound to AUTH_SECRET. maxAge 24 hours. Cleared on sign-out. |
4. OWASP mapping
| OWASP requirement | How Inbox Wingman implements it |
|---|---|
| Dedicated secrets-management solution | Vercel project Environment Variables (Sensitive) is the dedicated store for platform secrets. They are encrypted at rest by Vercel and injected only into the serverless runtime. TLS certificates are managed by Vercel, not stored in the app or the database. |
| Do not hardcode secrets | Application code reads process.env.* only. Repo search finds no live API keys, passwords, or private keys. Placeholders such as sk-placeholder are not production credentials. |
| Do not commit secrets to Git | .gitignore ignores .env* (except .env.example with dummy names). Local env files are not in version control. |
| Least privilege | Env access is limited to Vercel project members who can edit Environment Variables. Production and Preview are separate. The app never exposes secret values in HTML, APIs, or admin UI. Token decrypt is server-only for Gmail API or revoke. |
| Rotation and revocation | Platform secrets are rotated by replacing the Vercel env value and redeploying. Google refresh tokens are revoked via Google's revoke endpoint when a mailbox is disconnected or the account is deleted, then the ciphertext row is removed. Session JWEs expire in 24 hours; changing AUTH_SECRET invalidates all sessions. |
| Monitor access | Vercel records who added or updated each variable and when. Project activity retains deploys and env changes. The app logs secret.encrypt / secret.decrypt (kind, purpose, ok, time) without secret values. |
| Replace exposed secrets immediately | On suspected exposure: rotate the Vercel variable, revoke the provider key (Google Cloud OAuth client, OpenAI, Resend, Stripe/Paddle), redeploy, and revoke user Gmail grants if a refresh-token key is involved. Old values are not kept in the app. |
5. Logging and monitoring
- Platform. Each Vercel environment variable shows Added / Updated timestamps and the actor. Deployments and environment changes appear in Vercel project activity. Members review that history; values stay hidden.
- Application. Encrypt and decrypt of stored Google refresh tokens emit a structured log event
secret.encrypt/secret.decryptwithkind,purpose(gmail.persist,gmail.api,gmail.revoke),ok, andat. Ciphertext, plaintext, IVs, and keys are omitted. Events go to Vercel Runtime Logs and are retained with the project log stream. - Failed decrypts (wrong key or tampered payload) are logged with
ok: falseand throw; the request fails closed.
6. Rotation, revocation, incident
- Generate a new value at the provider (or
openssl rand -hex 32forTOKEN_ENCRYPTION_KEY/AUTH_SECRET). - Paste it into Vercel → Project → Settings → Environment Variables as Sensitive, Production and Preview. Save. The previous value is replaced, not shown again.
- Redeploy Production so every instance picks up the new value.
- Revoke the old provider credential. Disconnecting Gmail or deleting an account calls Google revoke and deletes
refresh_token_enc. - If
TOKEN_ENCRYPTION_KEYitself is rotated, existing ciphertext cannot be read; users reconnect Gmail (new encrypted refresh token). IfAUTH_SECRETis rotated, every session cookie is invalid and users sign in again.
7. What this control does not do
- No hardcoded production secrets in source or in Git.
- No
NEXT_PUBLIC_secret variables and no secret values in HTML or client JS. - No admin screen that displays env values or decrypted refresh tokens.
- Logs never include ciphertext, plaintext, IVs, keys, session tokens, or card data.
Related: Security, Privacy Policy, DPA.