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… | Use | Who decrypts |
|---|---|---|
| control the process (CI job, container, server) | seekrit run | your machine |
| have untrusted or agent workloads | egress proxy | your proxy |
| run Kubernetes | ESO chart | your cluster |
| are writing the app | a language SDK | your process |
| don't control the runtime (Vercel build env, Cloudflare bindings, a managed platform's env vars) | sync | seekrit'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.
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:
- 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.
- 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:
- Create a credential at the destination, scoped as narrowly as that platform allows.
- 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. - 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
| Destination | What it writes | Takes effect |
|---|---|---|
| Vercel | Project environment variables (encrypted), per deployment target | next build |
| Cloudflare Workers | A Worker's secret_text bindings — the slot wrangler secret put writes | immediately |
| Cloudflare Pages | A project's environment variables (secret_text), per deployment config | next deployment |
| Cloudflare Secrets Store | Account-level secrets that Workers bind by name | next Worker deploy |
| Railway | A service's variables, or an environment's shared variables | redeploy (triggered by default) |
| AWS Secrets Manager | One secret per name, or all of them as one JSON secret | next read by your app |
| AWS Parameter Store | SSM parameters under one path, SecureString by default | next read by your app |
| Render | One service's variables, or a shared environment group's | next deploy |
| Fly.io | An app's secrets, delivered to Machines as environment variables | next Machine boot (fly secrets deploy) |
| Northflank | A secret group's variables, inherited by the project's services and jobs | next deploy or restart |
| DigitalOcean | An App Platform app's or component's environment variables, encrypted | a new deployment, started by the push |
| Heroku | An app's config vars, delivered to every dyno as environment variables | immediately — a new release, and the dynos restart |
| Netlify | A site's environment variables, for the deploy contexts you name | next build and deploy |
| Bunnyshell | An environment's variables, or a project's — inherited by environments made later | next deployment of the environment |
| GitHub Actions | Repository, deployment environment, or organization Actions secrets | next workflow run |
| Google Secret Manager | One secret per name, or all of them as one JSON secret | next read by your app |
| LangGraph Platform | An Agent Server deployment's secrets, read as environment variables | a 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.
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
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.
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.
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.
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