seekrit
← all posts

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

SymptomCauseFix
403 on every nameTeam project, no team id on the connectionRe-create the connection with --team-id team_…
403 after it workedToken expired or its owner lost project accessNew token, re-connect
404 on the projectWrong prj_… idCheck the project's Settings; seekrit sync verify catches this early
400 mentioning gitBranchBranch set on a binding without preview in targetsAdd preview or drop --git-branch
Values pushed, site unchangedVercel injects at build timeRedeploy. 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.