exoDocs
Decisions (ADRs)

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

  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 (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

  • 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.

Source: docs/adr/0016-approved-stack-required.md

On this page