# 0017: Render is the runtime; the 1Password token is the only secret on the host

https://exo.now/docs/adr/0017-render-runtime

> Status: proposed (accepted when Samir merges the PR that adds it) · Date: 2026-10-24



# ADR 0017: Render is the runtime; the 1Password token is the only secret on the host [#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](https://exo.now/docs/adr/0016-approved-stack-required) (the approved stack is required)
* Supersedes: Railway as the managed-app runtime. Rejects Google Cloud Run (the Cloud Run plan,
  [#234](https://github.com/Interspace-Ventures/exo/pull/234), is closed).

## Context [#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](https://github.com/Interspace-Ventures/exo/pull/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) [#decision-samir-2026-10-24-0136-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`) [#enforcement-policy-v4-in-packagesexo-verifyliblevel1stackmjs]

* `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 [#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.
