seekrit
Docs/Third-party sync

Third-party sync

Most ways of getting secrets into a running app keep decryption on your side: seekrit run decrypts in your process, the egress proxy decrypts in your proxy, the SDKs decrypt in your code, and ESO decrypts in your cluster.

Some platforms don't let you run anything before your app starts. A Vercel build reads environment variables that Vercel already holds; a Cloudflare Worker reads bindings the platform injected before your code ran. There is no earlier point to inject them. Third-party sync is for exactly that case: seekrit stays the source of truth, and pushes the environment out whenever it changes.

Choose the right tool first

If you…UseWho decrypts
control the process (CI job, container, server)seekrit runyour machine
have untrusted or agent workloadsegress proxyyour proxy
run KubernetesESO chartyour cluster
are writing the appa language SDKyour process
don't control the runtime (Vercel build env, Cloudflare bindings, a managed platform's env vars)syncseekrit's sync engine

Sync is the last row, and it is the only feature in seekrit where our servers decrypt anything. Reach for it when the rows above don't apply.

caution

Enabling sync lets seekrit decrypt that environment. To push a value to any of the platforms below, something has to hold it in the clear, and sync runs when nobody is logged in — so the sync engine must be able to decrypt on its own.

This applies only to environments you explicitly enable it for, and only to the destination you named. Everything else stays zero-knowledge: seekrit cannot decrypt an environment that has no sync grant, and enabling one requires a key holder, so it can't be switched on server-side.

How it works

Enabling sync creates two things:

  1. A connection — the destination account: an API credential for Vercel, Cloudflare, AWS, or any other platform below. It is encrypted in your browser to a public key held by the sync engine, so the control plane stores ciphertext it cannot open.
  2. A binding — one environment → one destination. Creating it also creates a key grant: the environment's data key, wrapped in your browser to that same public key. This grant is what authorizes decryption, and it is an ordinary key grant — visible alongside your users and service tokens, and revocable the same way.
secret write ─▶ seekrit API ─▶ sync engine ──▶ destination
                               (decrypts here, only with a grant)

The private half of that keypair lives inside the sync engine and is never written to the database. Deleting the connection destroys it, which turns every grant made to it into ciphertext nobody can open — including us.

Every destination follows the same three steps, and each page below walks them for its own platform:

  1. Create a credential at the destination, scoped as narrowly as that platform allows.
  2. Add the connection with seekrit sync connect (or Sync → Add connection in the dashboard). The credential is read from stdin — never a flag, so it can't land in your shell history — and wrapped in your browser or terminal before it is sent.
  3. Bind an environment with seekrit sync enable. This is the step that shows the decryption disclosure and creates the key grant, so it has to be run by someone who can already read the environment.

--acknowledge-decryption is the terminal's version of the dashboard's disclosure: at a terminal you can leave it off and answer the prompt, but a non-interactive run must pass it, and either way the acknowledgment lands in the audit row. seekrit then pushes once immediately, and again on every change.

Destinations

DestinationWhat it writesTakes effect
VercelProject environment variables (encrypted), per deployment targetnext build
Cloudflare WorkersA Worker's secret_text bindings — the slot wrangler secret put writesimmediately
Cloudflare PagesA project's environment variables (secret_text), per deployment confignext deployment
Cloudflare Secrets StoreAccount-level secrets that Workers bind by namenext Worker deploy
RailwayA service's variables, or an environment's shared variablesredeploy (triggered by default)
AWS Secrets ManagerOne secret per name, or all of them as one JSON secretnext read by your app
AWS Parameter StoreSSM parameters under one path, SecureString by defaultnext read by your app
RenderOne service's variables, or a shared environment group'snext deploy
Fly.ioAn app's secrets, delivered to Machines as environment variablesnext Machine boot (fly secrets deploy)
NorthflankA secret group's variables, inherited by the project's services and jobsnext deploy or restart
DigitalOceanAn App Platform app's or component's environment variables, encrypteda new deployment, started by the push
HerokuAn app's config vars, delivered to every dyno as environment variablesimmediately — a new release, and the dynos restart
NetlifyA site's environment variables, for the deploy contexts you namenext build and deploy
BunnyshellAn environment's variables, or a project's — inherited by environments made laternext deployment of the environment
GitHub ActionsRepository, deployment environment, or organization Actions secretsnext workflow run
Google Secret ManagerOne secret per name, or all of them as one JSON secretnext read by your app
LangGraph PlatformAn Agent Server deployment's secrets, read as environment variablesa new revision, started by the push

Each page covers the credential to create, the least-privilege scope for it, how a binding addresses that platform, and what a push does and does not touch there. Everything on this page — naming, filtering, references, deletions, failure handling — works the same on every one of them.

note

Syncing to GitHub Actions? Read that page's opening warning first. GitHub is the one destination here that seekrit can inject into at runtime, so the published seekritdev/github-action is the better answer for most workflows — sync only reaches the cases an Action cannot.

Naming and filtering

By default, secret names are pushed verbatim. A binding can adjust that:

  • Prefix / suffix / case — e.g. prefix NEXT_PUBLIC_ or force upper case.
  • Rename — map individual names exactly. An explicit rename is used verbatim; prefix and case are not applied on top of it.
  • Include / exclude — glob allow/deny lists (DB_*, *_PASSWORD). Exclusion always wins over inclusion.

If two secrets would end up with the same destination name, the run fails and tells you which two. seekrit will not silently let one value shadow another.

From the CLI, sync enable takes --prefix, --include, and --exclude (comma-separated globs). Suffixes, case folding, and per-name renames are set in the dashboard.

seekrit sync enable --connection acme-production \
  --app storefront --env production --project prj_abc \
  --prefix NEXT_PUBLIC_ --include 'API_*,FEATURE_*' --exclude '*_PASSWORD' \
  --acknowledge-decryption
note

A name transform is the usual way to produce a name a destination rejects. Every platform has its own rules — Heroku reserves HEROKU_, GitHub reserves GITHUB_, Bunnyshell wants at least three characters, Secret Manager takes no slashes. seekrit's own names are legal almost everywhere, so it is a --prefix or a case fold that usually pushes one over the line. Each destination page lists its rules, and a name that breaks one is reported as a failure against that name alone — the rest of the environment still pushes.

Secret references

${OTHER_SECRET} references are expanded before the push, the same way seekrit run and the SDKs expand them. The destination receives the final value, not the reference — pushing the raw stored text would put a literal ${OTHER_SECRET} into your Vercel project.

A reference to a name that doesn't exist is left as written (so CI templating like ${GITHUB_SHA} passes through untouched). A reference cycle fails the run, because no value would be correct to push.

Composed environments

Sync pushes the effective environment — what your app actually resolves, including every composed group. That means the connection needs a grant on each composed group environment too, not only the application environment. The dashboard walks you through granting all of them; if one is missing, the run fails with no key grant for group "…" rather than pushing a partial environment.

Sync targets application environments. To sync a shared group, bind the application environments that compose it.

Branch environments

A branch environment can be synced like any other, and the pairing is a natural one: point a branch at a Vercel preview target with its git branch set, and each PR gets its own overlay downstream.

Two things follow from how branches inherit:

  • A write to the parent re-syncs its branches too, since a branch supplies only the values that differ.
  • When a branch expires, its binding goes with it and seekrit stops pushing. Values already at the destination are left alone — expiry removes the branch, not the copy the destination is holding. Delete those there if you need them gone.

Deletions

A binding's onDelete policy decides what happens when a secret disappears from seekrit:

  • delete (default) — remove it at the destination too, so the destination is a true mirror.
  • retain — leave it. Use this when something else also writes to that project and seekrit is not the only source.

A removal only ever touches what the binding itself owns: on Vercel, variables whose targets overlap the binding's; on Pages, the deployment configs it writes; on Render, the one service or group the binding names; on AWS and Google Secret Manager, the names under the binding's prefix or path. Two bindings can point at one project with different targets without stepping on each other. A name that is already gone at the destination counts as deleted rather than failing forever.

note

On Secrets Manager, deleting is AWS's scheduled deletion with its 30-day recovery window — a mistaken removal is recoverable in the console. The consequence is that the name stays reserved for those 30 days: if the secret comes back before the window closes, seekrit restores it rather than failing to recreate it.

Google Secret Manager has no such window. A removal deletes the secret and every version of it immediately and permanently, so use retain there if you would rather clean up by hand.

When a sync fails

Runs are recorded with per-name outcomes. A partial run is normal — no destination API is transactional, so some names can land while others fail — and only the failures are retried. Where a destination writes in batches (the Cloudflare Workers and Secrets Store bulk endpoints, Heroku's single PATCH) a rejected batch is reported against every name in it, because that is all the API tells us; re-pushing a value that did in fact land is harmless.

Retries back off exponentially. After five consecutive failures the binding stops and org admins get an email, because the destination is now serving stale values and nothing downstream will say so on its own. Fix the cause, then re-enable the binding or use Sync now.

From the CLI:

seekrit sync bindings                # mode, last run, last error
seekrit sync runs --binding syb_abc  # per-run history
seekrit sync run syb_abc             # push now; exits non-zero unless fully successful

sync run treats a partial result as a failure for exit-code purposes, so it can gate a deploy step.

note

A very large environment can take more than one run. Every connector works to a per-run request budget, sized against whichever ceiling binds first on that platform — the Worker subrequest cap, or the destination's own rate limit (Railway's is as low as 100 requests an hour on the free plan). A run that hits its budget reports that it has more to do and the engine re-runs immediately, so a big first sync is slower rather than silently partial.

A few failures are the same everywhere. The rest are per-destination, and each page has a troubleshooting table for its own:

  • no key grant for … — the grant was revoked, or the environment now composes a group the connection can't read. Re-enable sync for that environment.
  • the sync key grant may be stale — the environment was re-keyed without re-granting, so the connection holds a wrap around the wrong data key. Re-enable sync for that environment.
  • name collision: "…" and "…" both map to "…" — a name transform collapsed two names into one. Fix the rename, the prefix, or the filters; seekrit will not pick a winner.
note

A failure reason never carries a secret value. Error text is built from the destination's own message fields, never from a request body — and where a platform is known to echo a submitted value back in an error (Heroku, Netlify, Bunnyshell), the connector scrubs any value it just sent out of the message before it is stored on the run row.

Turning it off

Deleting a binding revokes that environment's grant to the connection (unless another binding still needs it). Deleting the connection revokes all of its grants and destroys its keypair.

Either way the values already at the destination stay there — seekrit stops updating them, it does not reach in and clean up. Remove them at the destination if you want them gone.

seekrit sync pause syb_abc               # stop pushing, keep the binding and its grant
seekrit sync disable syb_abc             # delete the binding and revoke its grant
seekrit sync disconnect acme-production  # delete the account, its bindings, and its keypair