Syncing environment variables to Vercel from one source of truth
How-to
A Vercel build reads environment variables that Vercel already holds. There's no earlier point where you can inject them, so if you want a secrets manager to be the source of truth for a Vercel project, it has to push into Vercel's project settings. With seekrit that's a connection and a binding:
printf '%s' "$VERCEL_TOKEN" | seekrit sync connect --name acme --team-id team_abc
seekrit sync enable \
--connection acme \
--app storefront --env production \
--project prj_abc \
--target production \
--acknowledge-decryption
From then on, every change to that seekrit environment is pushed to the
Vercel project as an encrypted environment variable, and the next deploy
picks it up. The rest of this post covers the setup in detail, preview
branches, and the trade you're making by pushing at all.
Why can't I just run seekrit run -- next build?
You can, if you build somewhere you control. On Vercel's own builders you don't control the command's environment; Vercel populates it from the project's env vars before your build command runs. Same for the serverless and edge functions at run time. So the values have to already be in Vercel.
Teams usually handle this with vercel env pull and vercel env add by hand,
which means the dashboard and the local .env.local files drift, and nobody
is sure which copy is current. Sync makes one place authoritative and treats
Vercel as a target.
Step 1: a Vercel token
In Vercel, Settings → Tokens. Give it access to the project you want to
sync. If the project belongs to a team, note the team id (team_…); a
personal-scope token can't write to a team project, and Vercel reports that
as a bare 403.
Vercel tokens carry their creator's full access. The least-privilege version is a machine user added only to the projects being synced.
Step 2: connect
printf '%s' "$VERCEL_TOKEN" | seekrit sync connect \
--name acme-production --team-id team_abc
The token is read from stdin rather than a flag, so it doesn't land in shell
history, and it's encrypted to the connection's key before it's sent. Vercel
is the default provider, so --provider vercel is optional.
Check the connection reaches the project before binding anything:
seekrit sync verify acme-production --project prj_abc
This does one GET /v9/projects/{id}, which proves the token works, reaches
this project, and has the right team id (or doesn't need one).
Step 3: bind an environment to a project and target
seekrit sync enable \
--connection acme-production \
--app storefront --env production \
--project prj_abc \
--target production \
--acknowledge-decryption
--target is a comma-separated list of production, preview,
development. One binding writes each variable to every target named, so a
single seekrit environment can feed production and preview.
seekrit pushes once immediately and then on every change to the environment.
Values are written with POST /v10/projects/{id}/env?upsert=true as
type: encrypted, one request per name.
What does --acknowledge-decryption mean?
This is the honest part. seekrit is end-to-end encrypted: normally the server holds only ciphertext and your CLI, browser, or runtime does the decrypting. Pushing to Vercel is the one case where no client of yours is present at the moment a plaintext is needed, so the sync engine decrypts in memory for the duration of a push. Enabling a binding requires someone who can already read the environment to create a key grant for the sync engine, and to acknowledge that this is happening. It's recorded in the audit log.
If you don't want that, don't use sync. Build somewhere you control the
process and use seekrit run, or resolve at runtime with the
JS SDK in a Node runtime. Sync is for the runtimes where
neither is possible, and Vercel's builders are the canonical example.
Preview deployments
--git-branch restricts preview writes to one branch:
seekrit sync enable --connection acme-production \
--app storefront --env staging \
--project prj_abc --target preview --git-branch staging \
--acknowledge-decryption
Vercel only accepts a git branch when preview is among the targets, so
seekrit only sends it then.
This pairs with branch environments: a seekrit branch
that inherits from dev and overrides one or two values (a database branch
URL, say) can be bound to a Vercel preview target with its git branch set.
Each PR gets its own overlay. When the branch expires, the binding goes with
it and pushes stop; values already on Vercel stay put.
Removing a variable
Deleting a secret in seekrit deletes it from Vercel too. Deletion needs Vercel's internal env-var id, so a run with removals lists the project's variables once and then deletes each. Only records whose targets overlap this binding's are touched, so two bindings can point at one project with different targets without deleting each other's variables.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
403 on every name | Team project, no team id on the connection | Re-create the connection with --team-id team_… |
403 after it worked | Token expired or its owner lost project access | New token, re-connect |
404 on the project | Wrong prj_… id | Check the project's Settings; seekrit sync verify catches this early |
400 mentioning gitBranch | Branch set on a binding without preview in targets | Add preview or drop --git-branch |
| Values pushed, site unchanged | Vercel injects at build time | Redeploy. seekrit doesn't trigger builds |
What Vercel can see
Everything you push. Vercel stores encrypted variables encrypted at rest,
and anyone who can read the project's environment variables in Vercel can
read them. That's true of any tool that pushes to Vercel, and it's exactly
what the key grant authorizes. Sync moves the source of truth; it doesn't
change what the destination is.
The full model, including name mapping and filtering, is in Third-party sync and the Vercel page.