exoDocs
Decisions (ADRs)

Decisions and roadmap

ADR 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
  • Decided by: Samir (owner)
  • Builds on: ADR 0016 (the approved stack is required)
  • Supersedes: Railway as the managed-app runtime. Rejects Google Cloud Run (the Cloud Run plan, #234, is closed).

Context

ADR 0016 fixed one choice per layer but left the Deploy layer as "Exo delivery" without naming the runtime, and left "the platform holds one read-only OP_SERVICE_ACCOUNT_TOKEN" without saying how the host holds it. Railway lost trust after its breach. Two replacements were drafted: Render (#227, a Blueprint per repo with the token as a sync: false env var) and Google Cloud Run (#234, the token in Google Secret Manager through IAM). An earlier revision of this ADR chose Cloud Run.

Decision (Samir, 2026-10-24 01:36 EDT)

"Render is the way. No reason for Google Cloud and that's not modern dev stack."

  1. Hosting is Render. Every managed app service is declared in a render.yaml Blueprint in its repository: web services for HTTP, worker and cron services for background and scheduled work, region virginia. Render is the runtime component under Exo delivery: Exo still registers delivery targets and owns the order Preview (Render preview environments) -> Staging -> Release Candidate -> Production.
  2. Secrets stay in each app's own 1Password vault, loaded at boot by the Exo Secure loader.
  3. The only secret on the host is one read-only 1Password service-account token (OP_SERVICE_ACCOUNT_TOKEN) per app per environment. Prefer a Render Secret File (set in the dashboard, mounted at /etc/secrets/<name>) where the app's loader reads a token file; otherwise a secret env var on the service, declared in render.yaml as sync: false so its value is only ever entered in the Render dashboard. Never a value in the Blueprint, never in a Blueprint env group. Render preview environments do not copy sync: false values, so Preview gets its own read-only token (never production's) from one dashboard-managed env group per app that holds only that token.
  4. No other secret in host settings. render.yaml envVars and Render env groups carry non-secret config only.
  5. Second secrets tool: Doppler only, never platform sync. If a second secrets tool is ever needed, it is Doppler, used as a run-time fetch (doppler run). Doppler's platform-sync / integrations mode, which copies secrets onto the host, is never used.
  6. Google Cloud Run is rejected. Why: Samir's call; Render is the modern developer stack (git-push deploys, Blueprints, preview environments, no cloud-project and IAM scaffolding to run); it is the fastest path off Railway; and the Render plan (#227) is already written, with its vault, item and DNS inventories. The trade-off accepted: the OP token sits in Render's own secret store (Secret File or sync: false env var) rather than a separate secret manager behind IAM. It is read-only, scoped to one vault per app per environment, and rotated on any exposure.

Enforcement (policy v4 in packages/exo-verify/lib/level1/stack.mjs)

  • APPROVED_STACK.deploy is "Exo delivery on Render". render.yaml / render.yml is the approved deploy config.
  • runtime.other-host flags railway.json/railway.toml, fly.toml, vercel.json, Procfile, an App Engine app.yaml, cloudbuild.yaml/.yml/.json, a Cloud Run (Knative) service YAML, a Cloud Run deploy command and the Cloud Run deploy GitHub Action, with a fix recipe that moves the service into a render.yaml Blueprint.
  • secrets.token-in-env flags, in render.yaml, an OP_SERVICE_ACCOUNT_TOKEN entry that is not exactly sync: false (a value, previewValue, generateValue, a fromGroup, or the token in an env group), and a literal service-account token anywhere in scripts or config.
  • secrets.platform-env flags, in render.yaml, any other secret-looking envVar set on a service (a literal value, previewValue, generateValue or sync: false), an env group holding secrets, and every fromGroup that pulls in such a group.
  • data.sql / data.other-store flag Render Postgres (databases:) and Render Key Value services in a Blueprint.
  • secrets.doppler-sync is unchanged: a doppler.yaml with sync or integration keys, a doppler secrets download command, or calls to Doppler's integrations or syncs API.
  • Like every rule in ADR 0016, these have no waivers and start in report mode.

Consequences

  • One Render workspace for Interspace, one Blueprint per repo, and one 1Password vault plus one read-only service account per app per environment. The per-app plan, cutover order and costs live in the Render plan (#227, docs/runbooks/render-cutover/).
  • Rotating an app secret: change it in 1Password, then redeploy. Rotating the bootstrap token: issue a new service-account token, replace the Secret File or sync: false value in the dashboard, and redeploy.
  • State on local disk (Hadron's capture and sync state) runs on a Render persistent disk for now. That is a Convex-only violation the scanner cannot see, tracked to move into Convex.
  • Railway configs remain in Construct (Rampart), Cosmograph, Dropship, Exo (Hadron), Moonshot, Observatory, Rampart and Universe; each carries the Render fix recipe. No repo uses Doppler.

Source: docs/adr/0017-render-runtime.md

On this page