# 0016: The approved infrastructure stack is required

https://exo.now/docs/adr/0016-approved-stack-required

> Status: proposed (accepted when Samir merges the PR that adds it); enforcement starts in report mode · Date: 2026-10-23



# ADR 0016: The approved infrastructure stack is required [#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](https://exo.now/docs/adr/0012-clerk-removed-convex-auth-everywhere),
  [ADR 0013](https://exo.now/docs/adr/0013-exo-is-mission-control-apps-pick-components)

## Context [#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 [#decision]

1. **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_TOKEN` and nothing else secret (core component `secrets`)
   * deploy = **Exo delivery**: Exo-registered delivery targets, Preview -> Staging -> Release
     Candidate -> Production (core component `runtime`). The runtime is Render (a `render.yaml`
     Blueprint per repo), and the OP token is the only secret on the host, a Render Secret File or a
     `sync: false` env var: [ADR 0017](https://exo.now/docs/adr/0017-render-runtime) (2026-10-24), policy v4.

   Specialty components (comms, payments, models, support, analytics, surfaces) sit on top of this
   stack and do not change it.
2. **One policy, everywhere.** `packages/exo-verify/lib/level1/stack.mjs` is the policy.
   * The shared `managed-repository.yml` workflow embeds it (step "Enforce EXO approved stack").
   * `exo-verify contract` and `exo-verify stack` run it, and so does Exo's own `pnpm check`
     (`check:stack`).
   * The Exo control plane records each App's latest check (`recordStackCheck`) and gates
     registration and delivery on it (point 5).
3. **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.
4. **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-brief` writes `STACK-FIX.md` (a plain brief the app's coding agent can
     follow) and `stack-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-fix` artifact whenever there are violations.
   * `exo-verify stack --fix` applies the fixes that are purely mechanical and safe. Today that is
     one: a secret declared in `project.yaml` without 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 with `getStackCheck`.
5. **Rollout: report mode until apps are clean.** One switch, `STACK_ENFORCEMENT` in `stack.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 runs
     `node scripts/sync-stack-policy.mjs`, which re-embeds the policy in the workflow and regenerates
     `convex/lib/stackEnforcement.ts` (the control plane's copy). `--check` fails if either drifts.
6. Secrets declared in `project.yaml` must say where they load from: `"source": "exo-secure"`
   or `"convex"`. `"platform"` is allowed only for `OP_SERVICE_ACCOUNT_TOKEN` (schema 1.10 adds the
   optional `source` field). A value that is not a credential (a URL, a flag, a recipient address)
   is declared `"secret": false` instead.

## Consequences [#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_URL` declaration (a Payload leftover) is removed from `project.yaml` and
  the repository template; the template now declares `OP_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-verify` CLI, so Entire's CI can call it unchanged.
