Secret env vars
You're on a video call. Sharing your screen to look up something unrelated, clicking into Vercel to find a different setting. Your team's webhook secret is sitting in plain text in the Environment Variables list. Nothing dramatic happens. The call ends. But that secret has been seen by whoever was watching, and you can't take it back.
Every environment variable on Vercel has a type: Config or Secret. A Config value stays readable in the dashboard for anyone with access, which suits a public URL prefix or a feature flag you'll want to check later. A Secret is write-only once you save it. Your code reads it at runtime, and the dashboard never shows it to anyone again, you included.
That's the upgrade we want for DROP_WEBHOOK_SECRET. While we're in there, we'll fix a quieter problem: right now Production and every preview share one value, so anyone holding a preview's copy holds the production key too.
Outcome
DROP_WEBHOOK_SECRET is stored as a Secret on Saturday with a Production value of its own, and your team requires a separate Production value for every Secret.
Hands-on exercise 2.1
The secret from the video call is burned, so we're rotating it. That makes this a delete-and-recreate job with two fresh values: one for Production, one for everything else.
Requirements:
- Open Saturday > Settings > Environment Variables and find
DROP_WEBHOOK_SECRET. Note the label on its row, Config or Secret - Generate two new values with
openssl rand -hex 32, one for Production and one for Preview and Development. Store both in a password manager, not a Slack DM - Delete the existing
DROP_WEBHOOK_SECRET - Add
DROP_WEBHOOK_SECRETagain and choose Secret under Type. Give Production the first value, and give Preview and Development the second - Redeploy production so the new value is the one in use
- Open Team Settings > Security & Privacy, find Separate Production Secret Values, and turn on Require Separate Values (this one needs the owner role)
Implementation hints:
- The Type choice sits in the create form alongside the key and value. Pick it before you paste anything.
- A Secret can carry a different value per environment, which is how one key ends up with two values. If you'd rather add the key twice, once targeting Production and once targeting Preview and Development, that works too.
- Secrets can target Development. If you remember a grayed-out switch whenever Development was selected, that rule belonged to the older Sensitive setting, and the docs note it's gone.
- The edit form has a Type choice as well, so for a variable you aren't rotating you can switch Config to Secret in place. The reverse doesn't exist: a saved Secret can't turn back into Config.
- Separate Production Secret Values replaced the older Enforce Sensitive Environment Variables policy, which forced every new Production and Preview variable to be stored as Sensitive. If your team had that one on, it's deprecated; review the new setting instead.
- The policy is per team. If you maintain multiple Vercel teams, you'll repeat this for each one.
Try It
After the redeploy completes, go back to Saturday > Settings > Environment Variables and find DROP_WEBHOOK_SECRET.
The rows should look something like this:
DROP_WEBHOOK_SECRET Secret Production
••••••••••••••
DROP_WEBHOOK_SECRET Secret Preview, Development
••••••••••••••
The values are replaced by dots. There's no reveal button. If you forget a value, you rotate it. That's the contract.
Now prove the rotation landed. Call the webhook on production with the old value from Lesson 1.1:
curl -X POST https://YOUR_DOMAIN/api/drop-launch \
-H "Content-Type: application/json" \
-H "x-webhook-secret: YOUR_OLD_SECRET" \
-d '{"dropId":"drop-003"}'The burned secret gets turned away:
{ "error": "Unauthorized" }Run it again with the new Production value. This time the webhook does its job:
{
"revalidated": true,
"dropId": "drop-003",
"revalidatedAt": "2026-05-26T16:04:51.208Z"
}Last, test the policy. Start adding a new Secret called TEST_SEPARATE_VALUES and try to give Production and Preview the same value. The dashboard stops you with Production must use a separate value. That's the policy doing its job. Cancel the form without saving.
Done-When
DROP_WEBHOOK_SECRETshows the Secret label in Saturday's Environment Variables list- Production has its own value, and Preview and Development share a second one
- The old secret returns
{ "error": "Unauthorized" }from/api/drop-launchon production, and the new Production value returns"revalidated": true - Team Settings > Security & Privacy has Require Separate Values turned on
- Grouping Production with another environment on a new Secret shows Production must use a separate value.
Troubleshooting
The webhook returns { "error": "Webhook secret is not configured" } after the redeploy.
The production deployment can't see DROP_WEBHOOK_SECRET at all. Most likely the recreated variable doesn't target Production, or the redeploy ran before you saved it. Check the targets on the rows, then redeploy again.
The new value returns { "error": "Unauthorized" }.
This is expected if you sent the Preview value to production. Each environment now has its own secret, so production only accepts the first value you generated. Grab the Production one from your password manager and try again.
I can't turn on Require Separate Values.
The policy needs the owner role on the team. If you're an admin or member, ask an owner to flip it. Everything else in this lesson works without it.
Solution
No code changes. app/api/drop-launch/route.ts reads process.env.DROP_WEBHOOK_SECRET the same way whichever type stores the value.
The state of the settings:
Saturday > Settings > Environment Variables
DROP_WEBHOOK_SECRET Secret Production (value 1)
DROP_WEBHOOK_SECRET Secret Preview, Development (value 2)
Team Settings > Security & Privacy > Separate Production Secret Values
Require Separate Values: Enabled
Up next: the webhook secret is locked down. The preview deployments that use it are still wide open. Let's fix that.
Was this helpful?