Integration credentials encrypted at rest

Every third-party credential — CRM token, SmartBuild password, Stripe key, QuickBooks tokens, Twilio subaccount token — is AES-256-GCM encrypted before it touches the database and decrypted only on the server.

What it is

Credentials are stored in *_encrypted columns on the organizations row (or in a binding config as enc:v1: strings). Reads decrypt with a 60-second in-memory cache. Legacy plaintext rows written before the encryption layer are detected on read and re-encrypted in place. A blob that is present but undecryptable reports 'not connected' rather than acting as a different identity.

Also called: API key security · where are my passwords stored · AES encryption · credential vault

See it
Integration credentials encrypted at rest
🔒ridgelinepostframe.com/project/encryp982
40×64 Shop
Framing · on schedule
Settings → Integrations with masked keys (abc123…7f4d) and a reveal eye, plus a small 'encrypted at rest' badge. Rendered by src/components/settings/IntegrationsTab.tsx. Sample data — no customer information appears here.
How it works
  1. 1The settings route encrypts at the boundary before the update statement runs.
  2. 2getOrgGhlCredentials / getOrgSmartbuildCredentials / getOrgQbCredentials / getStripeClient decrypt on read and cache for 60 seconds.
  3. 3A legacy plaintext value triggers a fire-and-forget re-encrypt of that row.
  4. 4A failed decrypt is logged loudly and the integration reads as disconnected.
  5. 5Saving new credentials invalidates every relevant cache immediately.
Why we built it

Two hazards are recorded in orgIntegrations.ts. The self-heal exists so the table migrates itself: 'Self-heals the table over time without a one-shot migration: anyone whose creds are read after this ships gets their row encrypted on the next read.' The fail-closed rule exists because the alternative is worse than an error: an undecryptable blob keeps the env fallback suppressed because 'surfacing "not connected" is more correct than silently acting as a different GHL identity.'

The problem
  • Third-party secrets previously sat in plaintext columns.
  • A key rotation without migration could have made the app act as the wrong tenant's identity.
Sound familiar?
What you get
Credentials are unreadable at rest.
Legacy rows are upgraded silently as they are used.
A decryption failure degrades to 'disconnected', never to the wrong account.
What's inside

See it on your own jobs

Twenty minutes, your numbers, no slide deck. We’ll build one of your real buildings in front of you and send you the estimate link at the end — yours to keep either way.

or keep browsing features →