exoDocs

Platform services

Exo Secure

Secrets load from each app's own 1Password vault at boot; platform env vars hold only one bootstrap token.

Source: ADR 0014, proposed on 2026-10-21 after Railway's security disclosure.

The rule

Exo Secure never stores secrets as platform env vars. Railway, Vercel and other hosts hold one bootstrap token per app (OP_SERVICE_ACCOUNT_TOKEN) and non-secret config. Everything else comes from the app's own 1Password vault when the server starts.

How it works

  • One vault per app environment. EXO uses Exo Staging and Exo Production. Each has one read-only 1Password service account that can see only that vault.
  • References, not values. Code lists what it needs as op://<vault>/<item>/<field> in src/lib/exo-secure/manifest.ts. References are safe to commit.
  • Loaded once, kept in memory. At boot, loadSecrets() resolves every reference with the official 1Password SDK and keeps the values in memory. They are never written back to the environment. Server code reads them with secret(name).
  • Fails closed in Production. If the token, the vault or a required secret is missing, the new deploy refuses to start and the previous one keeps serving.
  • Local development uses op run -- pnpm dev or a gitignored .env.secrets.local with EXO_SECURE_SOURCE=file.

Turning it on for an app

  1. Create the vault and a read-only service account scoped to it.
  2. Copy each secret into the vault under the item and field the manifest names.
  3. On the host, set OP_SERVICE_ACCOUNT_TOKEN, EXO_SECURE_SOURCE=1password and EXO_SECURE_VAULT=<vault name>.
  4. Redeploy, confirm the boot log says the secrets loaded from 1Password, then delete the old variables. The server warns by name about any that are still set.

Exo Key gateway tokens

Apps behind staging.exo.now used to trust one shared header secret. Exo Key now signs a short-lived token for each app (aud = exo-gateway:<app>, five minutes), and each app checks it against the public Exo Key keys. Apps hold no gateway secret, and a token for one app does nothing at another. The EXO_GATEWAY_TOKENS flag (off, dual, only) runs old and new side by side while each app moves over.

Checked on every build

EXO Verify's security command includes a platform-env check. Give it an export of the host's variables (for example railway variables --json) and it fails when any value looks like a raw secret, naming the variable but never printing the value. Without an export it skips.

railway variables --json | pnpm verify:platform-env -

Where secrets live

KindHome
Next server secrets (Twenty, gateway)The app's 1Password vault
Convex-only secrets (Bird, Exo Key signing keys, OAuth client secrets, AI keys)Convex deployment env, for now (ADR 0014, section 3)
URLs, hosts, flagsPlain env; not secrets

Cost

1Password service accounts come with the plan. Exo Secure makes one batched read per boot, so it costs nothing per app, deploy or request and stays well inside 1Password's rate limits. It is included in every plan, unmetered.

Source: content/docs/platform/exo-secure.mdx

On this page