Sync to Cloudflare
Cloudflare gives a Worker its secrets before your code runs — there is no earlier point at which seekrit could inject them. Three destinations live behind one platform here, and they behave differently enough to be worth choosing deliberately.
| Workers | Pages | Secrets Store | |
|---|---|---|---|
| What seekrit writes | A Worker's secret_text bindings — the slot wrangler secret put writes | A project's environment variables (secret_text), per deployment config | Account-level secrets that Workers bind by name |
| Addressed by | Worker script name | Pages project name + deployment configs | Store id + the scopes new secrets are created with |
| Token permission | Workers Scripts: Edit | Cloudflare Pages: Edit | Secrets Store: Edit |
| Takes effect | Immediately, on the Worker's current version | Next deployment (Pages binds at build time) | Next Worker deploy — a Worker needs a secrets_store_secrets binding to read one |
New to sync? Read Third-party sync first — the decryption grant, name mapping, deletions, and failure handling are the same on every destination.
1. Create a scoped API token
All three destinations share one connection shape: an API token plus the account ID it belongs to (32 lowercase hexadecimal characters, in the sidebar of any Cloudflare dashboard page). What differs is the token permission each needs.
In Cloudflare, go to My Profile → API Tokens → Create Token, use Create Custom Token, and grant the one account-level permission from the table above — not the global API key, which carries everything.
A connection is per product, because the token permissions are. Syncing to both Workers and Pages means two connections, each with its own scoped token. That is a feature rather than friction: a leaked Pages token cannot rewrite a Worker's secrets.
2. Add the connection
printf '%s' "$CLOUDFLARE_API_TOKEN" | seekrit sync connect \
--name acme-cloudflare --provider cloudflare-workers \
--account-id 0123456789abcdef0123456789abcdef
--account-id is required for all three Cloudflare providers, and seekrit checks
its shape before storing it — the alternative is a bare 400 from Cloudflare hours
later, inside a run nobody is watching. It cannot catch a zone id pasted by
mistake, which has the same shape; only the API can tell those apart, so verify a
destination before you bind to it.
3. Bind an environment
Workers
Name the script. A Wrangler environment is its own Worker, so deploying my-api
with --env staging creates my-api-staging — name that instead. Secrets land
on the Worker's current version immediately, with no redeploy:
seekrit sync verify acme-cloudflare --provider cloudflare-workers --script my-api
seekrit sync enable --connection acme-cloudflare \
--provider cloudflare-workers --script my-api \
--app storefront --env production --acknowledge-decryption
Verify lists the script's secret names — never values — which is the cheapest call that proves all three things a run needs: the token is valid, it is scoped to this account, and the Worker is deployed there.
Pages
Name the project and the deployment configs (production, preview), reusing
--target:
seekrit sync enable --connection acme-pages \
--provider cloudflare-pages --project my-site --target production,preview \
--app storefront --env production --acknowledge-decryption
Pages binds environment variables at build time. A pushed value reaches the running site on its next deployment — seekrit cannot force one, because a Pages deploy is a build, not a config reload. Workers are the opposite: values apply to the running Worker at once.
Secrets Store
Name the store (wrangler secrets-store store list, or the dashboard) and the
scopes new secrets are created with:
seekrit sync enable --connection acme-store \
--provider cloudflare-secrets-store \
--store-id 0123456789abcdef0123456789abcdef --scopes workers \
--app storefront --env production --acknowledge-decryption
--scopes takes a comma-separated list from workers, ai_gateway, dex,
access, containers, and websearch, and defaults to workers. Cloudflare
requires at least one at creation and cannot infer them, which is why a binding
states them.
Store secrets are account-level: a Worker still needs a secrets_store_secrets
binding in its Wrangler config to read one, and adding that binding is a deploy.
How a push behaves
Workers batches. PATCH …/secrets-bulk carries up to 100 upserts and
deletes per request, so a 500-secret environment is five requests rather than
five hundred. A null value deletes a name and an omitted name is left alone,
so a push never disturbs a binding it doesn't own. The cost is granularity: if
Cloudflare rejects a batch, every name in it is reported as failed, because that
is all the API tells us. Re-pushing a value that did in fact land is harmless.
Pages is a single PATCH of the project's deployment_configs. There is no
partial landing to report — it either applies or it does not. seekrit reads the
project first so removals are only sent where the key actually exists; Pages
redacts secret_text values on read, so that listing returns names only.
Secrets Store addresses every write by secret id, and seekrit only knows
names, so a run pages through the store's metadata (100 per page) to map them.
Creates are batched; updates and deletes are one request each. An update sends
the value alone — scopes and comment belong to the destination once a
secret exists, so widening a scope in the Cloudflare dashboard survives the next
sync. Secrets seekrit creates carry a comment marking them as managed.
All three share a per-run budget of 400 requests, sized against the Worker subrequest cap. A run that hits it reports more work and the engine re-runs immediately.
Cloudflare does not always disagree loudly. A handful of its endpoints
answer 200 with success: false and a populated errors array. seekrit
treats that as a failure rather than a landed push — otherwise a run would mark
secrets as delivered that never left the building.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
403 on every name | The token is missing that destination's one permission, or was issued for a different account | Check the permission in the table above, and that the account id matches the token's account |
404 on the Worker, project, or store | Renamed or deleted | Destinations are addressed by name, so a rename breaks the binding — update it. For a Wrangler environment, name the derived Worker (my-api-staging) |
400 right after adding a connection | A zone id was pasted instead of an account id | Both are 32 hex characters, so only the API tells them apart. Take the account id from the dashboard sidebar |
| Secrets pushed, Pages site unchanged | Pages binds at build time | Redeploy the project. seekrit does not start Pages builds |
| Store secret exists, Worker can't read it | The Worker has no secrets_store_secrets binding for it | Add the binding in wrangler.jsonc and deploy |
| A whole batch of names failed at once | Workers and Secrets Store write in batches | The API reports per batch, not per name. Fix the cause and re-run; the retry is idempotent |
See also
- Third-party sync — the shared model, naming and filtering, deletions
- CLI reference — every
seekrit syncflag