seekrit
Docs/Cloudflare

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.

WorkersPagesSecrets Store
What seekrit writesA Worker's secret_text bindings — the slot wrangler secret put writesA project's environment variables (secret_text), per deployment configAccount-level secrets that Workers bind by name
Addressed byWorker script namePages project name + deployment configsStore id + the scopes new secrets are created with
Token permissionWorkers Scripts: EditCloudflare Pages: EditSecrets Store: Edit
Takes effectImmediately, on the Worker's current versionNext 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.

note

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
note

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 alonescopes 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.

note

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

SymptomCauseFix
403 on every nameThe token is missing that destination's one permission, or was issued for a different accountCheck the permission in the table above, and that the account id matches the token's account
404 on the Worker, project, or storeRenamed or deletedDestinations 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 connectionA zone id was pasted instead of an account idBoth are 32 hex characters, so only the API tells them apart. Take the account id from the dashboard sidebar
Secrets pushed, Pages site unchangedPages binds at build timeRedeploy the project. seekrit does not start Pages builds
Store secret exists, Worker can't read itThe Worker has no secrets_store_secrets binding for itAdd the binding in wrangler.jsonc and deploy
A whole batch of names failed at onceWorkers and Secrets Store write in batchesThe API reports per batch, not per name. Fix the cause and re-run; the retry is idempotent

See also