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 StagingandExo 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>insrc/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 withsecret(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 devor a gitignored.env.secrets.localwithEXO_SECURE_SOURCE=file.
Turning it on for an app
- Create the vault and a read-only service account scoped to it.
- Copy each secret into the vault under the item and field the manifest names.
- On the host, set
OP_SERVICE_ACCOUNT_TOKEN,EXO_SECURE_SOURCE=1passwordandEXO_SECURE_VAULT=<vault name>. - 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
| Kind | Home |
|---|---|
| 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, flags | Plain 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.