Skip to content

Credentials & secrets

Where every secret lives, how it reaches the containers, how to rotate it, and what must never be in git or in a log line. Copilot-specific detail is in Copilot credentials; vendor app registration in ACC / Procore activation.

Where secrets live

Secret Set where Consumed by Notes
DATABASE_URL, POSTGRES_PASSWORD Dokploy → Compose app → Environment migrate, api, worker, backup POSTGRES_PASSWORD is required by the compose file (${POSTGRES_PASSWORD:?…})
JWT_SECRET (+ JWT_SECRET_OLD) Dokploy Environment api signs access/refresh tokens; ≥ 32 chars
APP_SECRET (+ APP_SECRET_OLD) Dokploy Environment api, worker AES-256-GCM key ring for MFA secrets, integration credentials, webhook signing secrets, SAML certs; HMAC for signed storage/point-cloud URLs and copilot confirmation tokens; ≥ 32 chars
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY (S3_ENDPOINT, S3_BUCKET, S3_FORCE_PATH_STYLE, STORAGE_DRIVER) Dokploy Environment api, worker, migrate (bucket creation) MinIO in the compose, or R2/S3 — see docs/storage-r2-s3.md
SMTP_HOST/PORT/USER/PASS/FROM Dokploy Environment api, worker unset = notifications stay in-app; invitations still work via Copy link
REDIS_URL, REDIS_KEY_PREFIX Dokploy Environment api, worker both must agree on the prefix or jobs never meet
APS_CLIENT_ID/SECRET, AUTODESK_CLIENT_ID/SECRET, PROCORE_CLIENT_ID/SECRET/BASE_URL Dokploy Environment api, worker vendor app credentials; the user tokens obtained through OAuth are stored encrypted in the database, never in env
GOOGLE_API_KEY, OPENROUTER_API_KEY, DEEPSEEK_API_KEY, COPILOT_* Dokploy Environment api see the copilot runbook
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET Dokploy Environment api unset = billing off, every workspace on the free plan
PLATFORM_ADMIN_EMAILS Dokploy Environment api allowlist for /api/admin/* (backup status, relation backfill, compliance evidence); fail-closed when unset
Planon base URL + API key, accounting credentials, SAML certificates, OAuth app client secrets, SCIM tokens, workspace API keys, webhook signing secrets entered in the UI / API by workspace or project admins api stored in Postgres: API keys hashed; the rest encrypted under APP_SECRET

Generate secrets with node -e "console.log(require('crypto').randomBytes(32).toString('hex'))". The full variable list with comments is apps/server/.env.example (the rotation and platform-admin names are listed there under "Operator settings").

The compose allowlist trap

Containers only see the variables the compose file's environment: block forwards. Setting a variable in Dokploy that the block does not list produces no error and no effect — the feature just stays off. Every copilot key hid behind this once — and so did every operator setting on this page: until the documentation pass of 2026-09-17 the anchor forwarded neither JWT_SECRET_OLD / APP_SECRET_OLD (a rotation had no old-key window), nor PLATFORM_ADMIN_EMAILS (the /api/admin/* routes were unreachable in Production), nor INTEGRATIONS_ENABLED, the Stripe pair, or the DWG_CONVERTER_PATH / FFMPEG_PATH / PDAL_PATH / POTREE_CONVERTER_PATH binaries. All are forwarded now, each tolerating the empty string compose renders for an unset variable. apps/server/tests/structural/compose-env-allowlist.test.ts pins every name on this page (MUST_FORWARD) and proves each parses "" safely; when you introduce a new server variable, add it to the environment: anchor in docker-compose.yml and to that test.

Rotation

  1. JWT_SECRET / APP_SECRET — move the current value to JWT_SECRET_OLD / APP_SECRET_OLD, set a new current value, save (Dokploy redeploys). Signing and encryption use the new key immediately; verification and decryption try the new key then the old one, so live sessions, signed URLs, MFA secrets and stored integration credentials keep working. Once the longest-lived artefact has expired (refresh tokens: REFRESH_TOKEN_TTL_DAYS, default 30) remove the _OLD variable. Rotating APP_SECRET without the _OLD window makes every encrypted credential unreadable — users would have to reconnect every vendor and re-enrol MFA.
  2. Vendor client secrets — replace in Dokploy; existing user tokens keep working until they expire, then users Re-authorize.
  3. Copilot keys — replace in Dokploy; the next request uses the new key. Removing all of them turns the feature off with an honest 503.
  4. Database / storage / SMTP passwords — rotate at the provider, update Dokploy, redeploy; the backup service reads POSTGRES_PASSWORD too.
  5. UI-managed secrets (API keys, SCIM tokens, OAuth app secrets, webhook secrets) — they are shown once at creation and cannot be re-shown; rotation = create new, switch the consumer, revoke old.

Every rotation that touches auth is worth a line in the audit log's context (the log records sign-ins, not env changes); note it in docs/PHASE-EXECUTION-LOG-2026-09.md.

What must never be in source control

  • apps/server/.env, any .env.* with values, Dokploy exports, docker-compose.override.yml with values. .env.example carries names and comments only.
  • Vendor keys, client secrets, JWT/APP secrets, database URLs with passwords, SMTP passwords.
  • Screenshots or logs containing bearer tokens, bz_… API keys, whsec_… webhook secrets, SCIM tokens, or the copilot confirmation token.
  • Customer data: exported IFCs, documents, photos, or database dumps.

What must never be in logs

  • The API redacts integration config before returning it (modules/integrations/redact.ts drops any key matching secret|token|password|passphrase|credential|private|signing|bearer|salt| hmac|key|cert|pem|jwt|otp|seed). Keep that pattern when adding config fields.
  • Copilot: prompts and tool results go to the model vendor by design; the confirmation token and the vendor keys are never logged. A vendor failure logs the vendor's message only ("copilot provider failed; trying the next one").
  • Never console.log a request body on an auth, integration or billing route.

Checking a deployment's secret posture

  • curl -s https://bim.addmedad.top/api/health → the api is up; it never returns configuration.
  • Workspace settings › Access review lists members without MFA and every API key.
  • GET /api/admin/compliance/evidence (platform admins) summarises the security controls in force.
  • Host ports: the base compose binds Postgres (5432), MinIO (9000/9001), the plain-HTTP nginx web (8080) and the documentation nginx docs (8081) to 127.0.0.1; the api has no host port at all. Only Traefik reaches web and docs, over dokploy-network. Verify from off-site with a TCP connect to 5432/9000/8080/8081 after any compose change.