How to sync environment variables with Vercel

Vercel gives every project three separate sets of environment variables and no way to tell whether they still agree with each other — or with the .env on the laptop of whoever set them up. The drift is silent until a preview deploy behaves differently from production.

Updated

Why Vercel environments drift

A Vercel project stores variables against three targets — Production, Preview and Development — and each one is edited independently. That is a reasonable design, and it produces a specific failure mode:

  • Someone adds a key to Production during an incident and never backfills Preview.
  • A rotated credential is updated in the dashboard, and the local .env that everyone copied months ago keeps the old one.
  • A new hire gets a .env from a teammate, which is a snapshot of that teammate's machine on the day they sent it.
  • Nothing anywhere records who changed a value, or when, or what it used to be.

The dashboard is the source of truth for what is deployed, but it is not a source of truth for the team — you cannot diff it, you cannot see its history, and you cannot hand someone read access to only the development set.

Pull what Vercel already has into one place

The first move is not to start retyping keys. Import what is there, so the starting state is real rather than reconstructed.

Create a Vercel access token from your account settings, then link the directory and import. Sink uses the token for that one request and never stores it:

sink login
sink init                          # writes .sink.json — ids only, safe to commit

sink import vercel development \
  --project my-app \
  --provider-env development \
  --dry-run

--dry-run prints exactly what would be created or changed and stops. Read that list before you run it for real — it is the cheapest moment to notice that the project you named is not the project you meant.

Then drop the flag to apply it, and repeat for the other targets into their own Sink environments:

sink import vercel development --project my-app --provider-env development
sink import vercel preview     --project my-app --provider-env preview
sink import vercel production  --project my-app --provider-env production

Keys that already exist are skipped by default. Pass --mode overwrite when you want Vercel's value to win — each overwrite mints a new version rather than destroying the old one, so an import that turns out to be wrong is recoverable.

Prefer not to paste the token into your shell history? It can come from stdin or the environment instead:

sink import vercel development --project my-app --token -
# or
export SINK_VERCEL_TOKEN=...
sink import vercel development --project my-app

Day-to-day: pull, diff, push

Once the environments live in Sink, the local .env stops being something you protect and becomes something you regenerate:

sink pull development     # writes .env, 0600 permissions
sink diff development     # what differs, values masked
sink push development     # send local changes back up

sink diff is the one that earns its place. It answers "is my local file still current?" — a question that, without it, is usually answered by debugging for twenty minutes. Add --show-values when you need to see what actually differs, and --exit-code when you want a script to fail on any difference:

sink diff production --exit-code || echo "production has moved on"

Onboarding, in one line

This is the part that changes shape completely. The new hire does not need a .env from anyone:

git clone [email protected]:you/my-app.git
cd my-app
sink login
sink pull development

Because .sink.json is committed, the clone already knows which workspace, team, project and environment it belongs to. There is no file to send, so there is nothing to send over Slack — which removes the most common reason credentials end up somewhere permanent. If you have already been sending them that way, see sharing a .env file securely.

In CI, and in Vercel's own build

CI has no keychain and no interactive login, so it reads an API key from the environment instead:

# in your CI provider's secret store
SINK_API_KEY=sk-...

# in the job
sink pull production --stdout > .env

One thing worth being clear about: Vercel builds still read Vercel's own environment variables. Sink is where the team's copy lives, gets versioned and gets audited — it does not replace what Vercel injects at build time. Treat Sink as the source of truth and Vercel as a deployment target you push to, the same way you would treat any other.

Keeping it honest

  • Re-run sink import vercel ... --dry-run occasionally. If it reports differences, something was changed in the dashboard and not written down.
  • Rotate in the provider first, then update Sink, then let everyone re-pull.
  • Use sink pull --version N to read an environment at an earlier version when you need to work out what a value used to be.
  • Give people the narrowest role that works — Viewer for anyone who only needs development.
Try it on your own project

Sink keeps every environment under AES-256-GCM envelope encryption, with roles, versions and an audit trail — and puts it back in your .env with one command. The free tier does not ask for a card.