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."
- Hosting is Render. Every managed app service is declared in a
render.yamlBlueprint in its repository:webservices for HTTP,workerandcronservices for background and scheduled work, regionvirginia. Render is theruntimecomponent under Exo delivery: Exo still registers delivery targets and owns the order Preview (Render preview environments) -> Staging -> Release Candidate -> Production. - Secrets stay in each app's own 1Password vault, loaded at boot by the Exo Secure loader.
- 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 inrender.yamlassync: falseso 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 copysync: falsevalues, so Preview gets its own read-only token (never production's) from one dashboard-managed env group per app that holds only that token. - No other secret in host settings.
render.yamlenvVars and Render env groups carry non-secret config only. - 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. - 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: falseenv 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.deployis "Exo delivery on Render".render.yaml/render.ymlis the approved deploy config.runtime.other-hostflagsrailway.json/railway.toml,fly.toml,vercel.json,Procfile, an App Engineapp.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 arender.yamlBlueprint.secrets.token-in-envflags, inrender.yaml, anOP_SERVICE_ACCOUNT_TOKENentry that is not exactlysync: false(avalue,previewValue,generateValue, afromGroup, or the token in an env group), and a literal service-account token anywhere in scripts or config.secrets.platform-envflags, inrender.yaml, any other secret-looking envVar set on a service (a literalvalue,previewValue,generateValueorsync: false), an env group holding secrets, and everyfromGroupthat pulls in such a group.data.sql/data.other-storeflag Render Postgres (databases:) and Render Key Value services in a Blueprint.secrets.doppler-syncis unchanged: adoppler.yamlwith sync or integration keys, adoppler secrets downloadcommand, 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: falsevalue 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
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
0022: Leave Railway for Render + 1Password broker
Status: proposed (draft PR; nothing deployed, no account created, no DNS changed) · Date: 2026-10-22 (revised 2026-10-22 22:00 EDT with Samir's decisions: Render, Observatory