Skip to content

Secrets

EnvironmentStorageHow they reach the running process
Local dev.env.local in each appBun + Vite read at startup.
Dev (deployed)Dokploy per-app Environment tabs (runtime) + the data-services Compose env; svelte-web build-time values go in Build Arguments (non-secret: PUBLIC_API_URL, Sentry DSN/org/project — SENTRY_AUTH_TOKEN is omitted in dev)Dokploy injects on container boot; build args bake at image build.
Staging + ProductionGCP Secret Manager, automatic (multi-region) replication; GCS access is Workload Identity, so no static storage keys exist in either tierInjected into Cloud Run at deploy; rotation is a secret-version bump. Decided, not provisioned yet — see Production landscape.
Native mobile buildsPer-app build config (buildConfigField / build settings) + gitignored signing/keystore filesCompiled into the binary at native build time; client-safe values only.
CategoryServer-onlyClient-safe
DatabaseDATABASE_URL, DIRECT_URL
AuthBETTER_AUTH_SECRET, GOOGLE_CLIENT_SECRETGOOGLE_CLIENT_ID (public on OAuth consent)
Cache / queuesVALKEY_URL (incl. password)
SearchMEILISEARCH_ADMIN_API_KEYMEILISEARCH_SEARCH_API_KEY (read-only, scoped to public indexes)
StorageAWS_SECRET_ACCESS_KEY
Image proxyIMGPROXY_KEY, IMGPROXY_SALTimgproxy URLs (signed)
EmailRESEND_API_KEY
ChatGETSTREAM_API_SECRETPUBLIC_GETSTREAM_API_KEY (public-by-design, gated by server-issued user tokens)
NotificationsKNOCK_SECRET_API_KEY, KNOCK_FCM_CHANNEL_ID, KNOCK_APNS_CHANNEL_IDPUBLIC_KNOCK_API_KEY, PUBLIC_KNOCK_FEED_CHANNEL_ID
MapsGOOGLE_MAPS_API_KEY (server)Android Maps key in the Android project (iOS uses MapKit — no key); restricted by package name + signing-cert SHA-1
AnalyticsSENTRY_AUTH_TOKENSENTRY_DSN, PUBLIC_POSTHOG_KEY
AIGEMINI_API_KEY, REPLICATE_API_TOKEN, REPLICATE_WEBHOOK_SIGNING_SECRET, AI_STAGING_USER_HASH_SECRET
PaymentsSTRIPE_SECRET_KEY, STRIPE_WEBHOOK_SIGNING_SECRET— (hosted Checkout, so no publishable key ships — there is no Stripe.js on the client)
Fiscal / CFDIFACTURAPI_SECRET_KEY (the FACTURAPI_PRODUCT_KEY / _UNIT_KEY / _PAYMENT_FORM overrides are SAT catalog values, not secrets)
Fiscal dataPII_ENCRYPTION_KEYnon-re-issuable, see below

Every secret above this line can be replaced. Lose RESEND_API_KEY and you issue a new one; lose AI_STAGING_USER_HASH_SECRET and the old pseudonyms stop correlating, which costs nothing anyone can name. The rotation procedure below assumes exactly that, and its first step — “issue a new credential in the provider dashboard” — is meaningless for a key with no dashboard behind it.

PII_ENCRYPTION_KEY is the first secret in this repo that does not work that way.

It is a data-encrypting key. It is self-generated (openssl rand -base64 32), no provider holds a copy, and it is the only thing that can read tax_profiles.{taxId,legalName,zip,email} and cfdi_documents.{receiverTaxId,receiverLegalName} — the RFC, razón social and fiscal domicile of every paying customer (FSR-014, packages/api/src/services/pii-crypto.ts).

Consequences that follow from that, and are not optional:

  • An escrowed copy must exist outside the deployment platform, before the first encrypted row does. Dokploy’s env UI is the live store today, and a platform account problem must not be the same event as permanent data loss.
  • Record the vault and item name here — never the value. Escrowed copy lives in: ⚠️ NOT YET RECORDED. Left visibly blank rather than hidden in a comment: this is the one line that makes the escrow real rather than intended, and an unfinished sentence on the page is harder to overlook than a TODO nobody greps for.
  • Never derive it from another secret, and never reuse it. Coupling it to something with a routine rotation cadence is exactly the mistake documented under BETTER_AUTH_SECRET below.
  • Rows written before FSR-014 shipped are plaintext at rest and stay readable with or without the key, which is what makes a rollback safe — it does not make the key optional for anything written since.
  1. Issue a new credential in the provider dashboard (Better Auth, Resend, Knock, etc.).
  2. Update the Dokploy env var on the dev environment first.
  3. Redeploy and verify nothing breaks.
  4. Update STG — a new Secret Manager version on the rec-stg secret, then redeploy and verify. Skipping STG defeats the reason it exists: rotation is exactly the class of change that should break on a rehearsal tier rather than in production.
  5. Update production the same way (once it exists).
  6. Revoke the old credential in the provider dashboard.

For BETTER_AUTH_SECRET rotation: rotating the secret invalidates all sessions. Coordinate with the team so users get a single sign-out event instead of a silent log-out mid-flow.

For IMGPROXY_KEY / IMGPROXY_SALT rotation: all currently rendered HTML pages and SSR caches still hold URLs signed with the old key. Either (a) keep the old key alongside the new for an overlap window (imgproxy supports multiple keys), or (b) accept a short window of broken images while pages re-render.

For REPLICATE_WEBHOOK_SIGNING_SECRET rotation: fetch the current default secret from Replicate, update apps/api, and redeploy the API before creating new predictions. Webhooks are verified against the exact raw body with a five-minute replay window. REPLICATE_API_TOKEN belongs only on the worker; apps/api needs the webhook signing secret but not the API token.

Any client-side key compiled into a native binary ships to user devices. Anyone with the IPA / APK can extract these keys (via strings, decompilation, or sniffing the first network request).

Provider-side restrictions are mandatory before public ship. See Architecture → Deployment for the per-key restriction recipes.

Server-only secrets like GETSTREAM_API_SECRET and KNOCK_SECRET_API_KEY must never be shipped to a mobile bundle. They belong in apps/api’s env (repo-root .env.local locally, the Dokploy api app’s Environment tab on the deployed dev env) — never in any client app.

  • No .env* (except .env.example) committed.
  • No *_vault.yml (decrypted) committed.
  • No *.tfvars (real values) committed.
  • No google-services.json committed.
  • No *.pem / *.jks committed.
  • CI logs scrubbed of secret values (Dokploy build logs stay on the VPS; GitHub Actions secrets are masked).
  • Sentry beforeSend scrubber is installed in every Sentry init.
  • Mobile builds have provider-side API key restrictions applied.