# Sync to Render

Render injects environment variables into the process it starts, and a service
picks up a change on its next deploy. A binding writes one of two places: a
single service's own variables, or a shared environment group that every linked
service reads.

| At a glance | |
| --- | --- |
| **What seekrit writes** | One service's environment variables, or an environment group's |
| **Addressed by** | Service id (`srv-…`, or `crn-…` for a cron job) or environment group id (`evg-…`) |
| **Connection carries** | The API key alone — there is no account or team id |
| **Token permission** | An account API key. Render has no narrower scope |
| **Takes effect** | Next deploy of the service. A group change starts a deploy of every linked service with autodeploy on |
| **Value visibility** | Readable by anyone with access to the Render workspace |

Both destinations are the same API with a different noun in the path, so they
share one connection. Point as many bindings at it as you have services and
groups.

New to sync? Read [Third-party sync](/docs/guides/third-party-sync) first — the
decryption grant, name mapping, deletions, and failure handling are the same on
every destination.

## 1. Create an API key

In Render, go to **Account Settings → API Keys** and create a key.

> **Warning:** A Render API key carries the permissions of the user who created it, and Render has no narrower scope to grant — the key reaches every workspace that user can. Mint it from an account whose access you are willing to hand the connection, and name the connection after the workspace it is for.

```bash
printf '%s' "$RENDER_API_KEY" | seekrit sync connect \
  --name acme-render --provider render
```

There is no account or team id to supply: every Render endpoint seekrit calls
names its resource by id, so the binding carries the whole address.

## 2. Bind an environment

**A service** — pass the service ID from its dashboard URL (`srv-…`, or `crn-…`
for a cron job):

```bash
seekrit sync verify acme-render --provider render --service srv-abc123

seekrit sync enable --connection acme-render \
  --provider render --service srv-abc123 \
  --app storefront --env production --acknowledge-decryption
```

**An environment group** — pass the group ID (`evg-…`) instead:

```bash
seekrit sync enable --connection acme-render \
  --provider render --env-group evg-xyz789 \
  --app storefront --env production --acknowledge-decryption
```

Pass one or the other; which flag you use is what picks the destination. seekrit
refuses an `evg-` id in `--service` and a `srv-`/`crn-` id in `--env-group`,
because pasting one into the other's field is otherwise a 404 hours later inside
a run nobody is watching.

> **Note:** **Render does not redeploy on an API-driven change.** A service picks up new values on its next deploy — start one from the Render dashboard if you need a value live immediately. An environment group goes the other way: changing one starts a deploy for every linked service that has autodeploy enabled. A group binding's blast radius is the link list, not one service. And a variable set directly on a service still **wins** over the same name coming from a group.

## How a push behaves

- **One request per name**, `PUT …/env-vars/{key}`. seekrit deliberately does
  **not** use Render's bulk endpoint: `PUT /v1/services/{id}/env-vars` replaces a
  service's *entire* variable set, which would delete every variable the binding
  does not own — the ones a person set in the dashboard, and the ones the
  binding's own filters exclude — and would make `onDelete: retain` a lie.
  Addressing each key individually is the only version that touches exactly what
  the binding claims.
- **That makes the request count the per-run ceiling.** A run is capped at 400
  operations, so an environment larger than that syncs across several runs, back
  to back.
- **Nothing reads a successful response body.** Render echoes values back — the
  env-group upsert returns the whole group in plaintext, and both list endpoints
  return values. seekrit parses no 2xx body at all, so those never materialize
  inside the sync engine; only an error body is read, and only for its message.
- **seekrit never asks Render to generate a value.** The write is `{ value }`,
  never `{ generateValue: true }` — seekrit is the source of truth.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| `401` or `403` | The API key was revoked, or the user who created it lost access to that workspace | Render keys are per user, so someone leaving can break a connection. Mint a new key from an account with access |
| `404` on the service or group | It was deleted | Render addresses these by id, so a **rename is safe** — only deletion breaks a binding |
| An id is rejected when enabling | An `evg-` id was passed to `--service`, or a `srv-`/`crn-` id to `--env-group` | Use the flag that matches the id. Which flag you pass is what picks the destination |
| Values pushed, service unchanged | Render does not redeploy on an API change | Start a deploy from the dashboard. seekrit does not trigger one |
| A group change deployed more than you expected | Every linked service with autodeploy redeploys | That is the group's blast radius — bind a service directly if you want it narrower |
| A value at the destination doesn't match the group | A service-level variable shadows the group's | Render's precedence, not seekrit's. Remove the service-level one |
| Run reported partial on a big environment | The 400-operation budget | Nothing to do — the engine re-runs immediately until it drains |

## See also

- [Third-party sync](/docs/guides/third-party-sync) — the shared model, naming and filtering, deletions
- [CLI reference](/docs/reference/cli#third-party-sync) — every `seekrit sync` flag
