Decisions and roadmap
ADR 0016: The approved infrastructure stack is required
- Status: proposed (accepted when Samir merges the PR that adds it); enforcement starts in report mode
- Date: 2026-10-23
- Decided by: Samir (owner)
- Builds on: ADR 0012, ADR 0013
Context
An agent working on Cosmograph proposed moving its database to Neon instead of Convex. Nothing stopped it: the approved choices lived in docs and memory, not in the contract. Samir's ruling (2026-10-23): "this needs to be required at Exo level... the whole point of Exo and the only way we can reliably support it is if we use a strict set of infra."
Decision
-
One approved choice per layer, for every app Exo manages:
- data = Convex (core component
data) - identity = Exo Key, on Exo Auth / Convex Auth (core component
identity) - secrets = Exo Secure: the app's own 1Password vault, loaded at boot; the platform holds
one read-only
OP_SERVICE_ACCOUNT_TOKENand nothing else secret (core componentsecrets) - deploy = Exo delivery: Exo-registered delivery targets, Preview -> Staging -> Release
Candidate -> Production (core component
runtime). The runtime is Render (arender.yamlBlueprint per repo), and the OP token is the only secret on the host, a Render Secret File or async: falseenv var: ADR 0017 (2026-10-24), policy v4.
Specialty components (comms, payments, models, support, analytics, surfaces) sit on top of this stack and do not change it.
- data = Convex (core component
-
One policy, everywhere.
packages/exo-verify/lib/level1/stack.mjsis the policy.- The shared
managed-repository.ymlworkflow embeds it (step "Enforce EXO approved stack"). exo-verify contractandexo-verify stackrun it, and so does Exo's ownpnpm check(check:stack).- The Exo control plane records each App's latest check (
recordStackCheck) and gates registration and delivery on it (point 5).
- The shared
-
No waivers. A violation can only be fixed. Samir's ruling (2026-10-23): there is no exception path, in this repo or any other. Existing violations are fixed before strict enforcement turns on.
-
Apps fix themselves. "We need a way for apps to fix themselves if they're wrong." Every rule carries a machine-readable
fix: the approved replacement, concrete steps and a docs anchor (docs/approved-stack.md#fix-*).exo-verify stack --fix-briefwritesSTACK-FIX.md(a plain brief the app's coding agent can follow) andstack-fix.json(each violation with rule, file, line, package or variable, and its fix).- In CI the step prints the brief, adds it to the job summary, annotates each violation, and
uploads both files as the
exo-stack-fixartifact whenever there are violations. exo-verify stack --fixapplies the fixes that are purely mechanical and safe. Today that is one: a secret declared inproject.yamlwithout an approved source gets"source": "exo-secure". It never rewrites application code, and it leaves provider variables (DATABASE_URL,CLERK_*) alone because those must be removed, not relabelled.- When a failing check reaches the control plane, Exo stores each violation with its fix on the
App (
stackCheck.violations); the App's owner or agent reads it back withgetStackCheck.
-
Rollout: report mode until apps are clean. One switch,
STACK_ENFORCEMENTinstack.mjs:'report'(the default for now): violations print a warning and the fix brief. CI passes, and the control plane does not block registration or delivery.'enforce': violations fail CI; the control plane refuses to register a new App without a passing check, refuses to enable delivery while the last check fails, and delivers to Release Candidate or Production only after a passing check.- Samir flips it to
'enforce'once the managed apps are clean, then runsnode scripts/sync-stack-policy.mjs, which re-embeds the policy in the workflow and regeneratesconvex/lib/stackEnforcement.ts(the control plane's copy).--checkfails if either drifts.
-
Secrets declared in
project.yamlmust say where they load from:"source": "exo-secure"or"convex"."platform"is allowed only forOP_SERVICE_ACCOUNT_TOKEN(schema 1.10 adds the optionalsourcefield). A value that is not a credential (a URL, a flag, a recipient address) is declared"secret": falseinstead.
Consequences
- Known violations are fixed, not waived, before enforce mode: Cosmograph (Postgres + Express and Clerk), Construct (Clerk), and Exo itself (changelog recipient variables declared as secrets; Hadron Dropship's Redis client). The org-wide scan lists the rest.
- Exo's stale
DATABASE_URLdeclaration (a Payload leftover) is removed fromproject.yamland the repository template; the template now declaresOP_SERVICE_ACCOUNT_TOKEN. - Adding or changing a rule, a fix recipe or the enforcement mode is an Exo PR, followed by
node scripts/sync-stack-policy.mjs. - The CI wrapper can only run when GitHub Actions runs for the app repo. The same check runs from
the
exo-verifyCLI, so Entire's CI can call it unchanged.
Source: docs/adr/0016-approved-stack-required.md
0014: Exo Secure loads secrets from 1Password at boot; no secrets in platform env vars
Status: proposed (draft PR; owner decision pending on the open items below) · Date: 2026-10-21
0017: Render is the runtime; the 1Password token is the only secret on the host
Status: proposed (accepted when Samir merges the PR that adds it) · Date: 2026-10-24