# Sync to Netlify

Netlify reads a site's environment variables during a build and hands them to
functions and edge functions at runtime — from its own copy, before anything of
yours runs. A binding writes one site's variables, for the deploy contexts it
names.

| At a glance | |
| --- | --- |
| **What seekrit writes** | A site's environment variables, per deploy context |
| **Addressed by** | The site's **API ID** (a UUID) + one or more contexts |
| **Connection carries** | The token, and the **team** whose variables it writes |
| **Token permission** | A personal access token whose user can change the team's environment variables |
| **Takes effect** | Next build and deploy |
| **Value visibility** | Write-only, when created as a Netlify secret (the default) |

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 a personal access token

Netlify issues them under **User settings → Applications → Personal access
tokens**. A token carries its user's access, so the least-privilege setup is a
machine user in only the team it syncs.

Unlike most connections here, a Netlify one has a second half to state: the
**team**. Netlify keeps environment variables on the account, not the site —
every endpoint is `/accounts/{account_id}/env`, with the site as a filter — and
a personal access token belongs to a user who may sit in several teams, so it
cannot say which one to write. Pass the slug from your dashboard URL
(`app.netlify.com/teams/<slug>`), or the account ID; Netlify treats the two as
interchangeable wherever `{account_id}` appears.

```bash
printf '%s' "$NETLIFY_AUTH_TOKEN" \
  | seekrit sync connect --name acme-netlify --provider netlify --account-id acme
```

## 2. Bind an environment

A Netlify value is keyed by **site and deploy context**, so a binding names
both. `--target` takes the contexts — `production`, `deploy-preview`,
`branch-deploy`, `branch`, or `dev` — and defaults to `production`:

```bash
seekrit sync verify acme-netlify --provider netlify \
  --netlify-site 3970e0fe-8564-4903-9a55-c5f8de49fb8b

seekrit sync enable --connection acme-netlify --provider netlify \
  --netlify-site 3970e0fe-8564-4903-9a55-c5f8de49fb8b \
  --target production,deploy-preview \
  --app storefront --env production --acknowledge-decryption
```

For one named branch, pass `--target branch` with `--git-branch staging`. A push
writes **only** the contexts you list: the same variable's other contexts, and
every variable this binding does not manage, are left exactly as they are — which
is what makes it safe to point at a site that already has variables set by hand.

Netlify's `all` context is deliberately not offered. A **secret** value must be
set against explicit contexts, and Netlify's own endpoint is reported to fail
outright on `context: "all"`. Naming the contexts you mean is what you want here
anyway — a binding already exists to map one seekrit environment onto one deploy
context.

> **Warning:** **The site is named by its API ID, not its name.** Netlify shows it under Project configuration → General → Project information, and it is a UUID. seekrit refuses anything else on purpose. The environment variable endpoints take the site as a query parameter, where Netlify resolves no names — and a site it cannot resolve does not fail the request. The variables are created on the **team** instead, shared by every site in it. `seekrit sync verify` also checks that the site really belongs to the connection's team, which is the other half of that mistake.

> **Note:** **Values are pushed as Netlify secrets.** Netlify's Secrets Controller makes a variable write-only: after it is set, the value cannot be read back through the UI, CLI, or API. seekrit creates variables that way by default, which is what you want for something a secrets manager owns. Three things follow from Netlify's rules. The flag can only be set when the variable is **created** — Netlify will not add it to one that already exists, and never removes it — so a name you already set by hand keeps whatever it has. A secret cannot carry the post-processing scope (a secret in snippet injection would be served to the browser), so seekrit sets `builds`, `functions`, and `runtime`. And values in the `dev` context stay readable by design, since local development needs them. If your plan does not include Secrets Controller, pass `--no-netlify-secret`.

## How a push behaves

- **A create carries every context at once**; an update sets one context's value
  at a time. So a variable seekrit creates costs one request, and a change to an
  existing one costs a request per context it writes.
- **A removal reads the variable's value ids first**, then drops the ones for the
  contexts this binding owns — or the whole variable when it owns them all.
- **A run is capped at 400 operations**, sized against Netlify's rate limit of
  **500 requests per minute per user**, which binds well before the Worker
  subrequest cap and is shared with everything else that token does.
- **A value Netlify echoes back in an error is scrubbed** before the reason is
  stored on the run row.

Netlify binds environment variables at build time, so a pushed value reaches the
running site on its **next deployment** — a sync run does not start one.

## Name and value rules

> **Note:** Netlify will not accept every name seekrit will. Variable names take only letters, numbers, and underscores, must **start with a letter** — where seekrit also allows a leading underscore — and cannot begin with `NETLIFY_`, which Netlify reserves for itself. Keys stop at 255 characters and values at **5,000**. A name that breaks one of those is reported as a failure against that name alone; the rest of the environment still pushes. Watch for it if you use a `--prefix`.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| The site ID is rejected when enabling | A site name or `.netlify.app` address was pasted | Use the API ID (a UUID) from Project configuration → General |
| Variables appeared on **every** site in the team | A site id Netlify could not resolve — the failure mode the strict check exists to prevent | Delete the team-level variables, then re-bind with the API ID |
| Verify fails saying the site isn't in this team | The connection's `--account-id` is a different team | Re-create the connection with the team that owns the site |
| `401` on every name | The token was revoked | Mint a new personal access token |
| `403` | The token's user can't change that team's environment variables | Give the machine user the right team role |
| A create fails mentioning secrets or scopes | The plan has no Secrets Controller | Pass `--no-netlify-secret` to create readable variables |
| A variable set by hand is still readable | The secret flag only applies at creation, and Netlify won't add it later | Delete it at Netlify and let the next sync recreate it |
| One name failed, rest landed | The mapped name starts with a digit or underscore, begins `NETLIFY_`, or the value is over 5,000 characters | Fix the prefix or rename |
| Values pushed, site unchanged | Netlify binds at build time | Trigger a deploy. seekrit does not start builds |

## See also

- [Third-party sync](/docs/guides/third-party-sync) — the shared model, naming and filtering, deletions
- [Branch configs](/docs/guides/branches) — per-PR environments that pair with deploy previews
- [CLI reference](/docs/reference/cli#third-party-sync) — every `seekrit sync` flag
