On 2026-04-19, Vercel disclosed a security incident caused by a compromised Context.ai OAuth app. Environment variables not flagged as Sensitive may have been read in plaintext. If you see a Need To Rotate badge in your Vercel dashboard, rotate within hours — not days. This post is the exact sequence I ran on 32blog.com two days after disclosure, including two mistakes I almost made.
If you've been running on Vercel Pro for a while, you've probably never thought about your environment variable posture. Neither had I, until I logged in and saw orange Need To Rotate badges next to RESEND_API_KEY, UPSTASH_REDIS_REST_URL, and UPSTASH_REDIS_REST_TOKEN. The reflex is to panic-click "rotate" on each row. That is not what Vercel actually wants you to do, and doing it blindly will leave you with a duplicate orphan variable I'll show you how to avoid.
What Actually Happened
Vercel's official bulletin on 2026-04-19 confirms unauthorized access to internal systems through a Vercel employee's Google Workspace account. The attack chain:
- Context.ai OAuth app compromised (supply-chain attack on a third-party tool the employee had connected to Workspace)
- Employee session tokens exfiltrated via the OAuth grant
- Attacker accessed internal Vercel systems for a window that reportedly stretches back to ~June 2024
- Scope: "A limited subset of customers whose non-sensitive environment variables stored on Vercel (those that decrypt to plaintext) were compromised."
ShinyHunters has claimed responsibility, as reported by BleepingComputer. GitGuardian's analysis notes that marketplace integrations (Upstash, Resend, Supabase, etc.) auto-provision env vars in plaintext by default — which is why those vars are the primary risk surface.
Vercel's explicit recommendation: "Review and rotate environment variables that were not marked as 'sensitive'."
Which Env Vars Did Vercel Badge
In the Vercel dashboard → Settings → Environment Variables, affected rows now show an orange Need To Rotate badge. On 32blog.com, three rows were flagged:
RESEND_API_KEY— used by my contact form via the Resend transactional email APIUPSTASH_REDIS_REST_URLUPSTASH_REDIS_REST_TOKEN
Three other variables were not badged:
GA_SERVICE_KEY_BASE64— Google service account key for GA4 (not badged, but it is a secret)GA_PROPERTY_ID— numeric property ID, public by designVERCEL_DEEP_CLONE— a boolean flag that controls Vercel's git clone depth
The absence of a badge does not mean the variable is safe. It means Vercel's internal records don't flag that specific row for rotation — but if you have any secret stored in plaintext, the defensive move is to migrate it to Sensitive storage anyway.
The Rotate vs. Remove Decision
Before touching anything, ask: do I still use this?
| Scenario | Action |
|---|---|
| Variable is in active use | Rotate. Generate new key at source, update in Vercel, redeploy. |
| Variable is unused (code reference removed, service no longer needed) | Remove. Delete from Vercel, disconnect the Marketplace integration. |
| Variable auto-provisioned by a Marketplace integration | Remove via integration. Uninstalling the integration deletes its env vars atomically. |
Rotating an unused variable wastes time and leaves a dead key in your secrets manager. Removing a still-used variable breaks production. Grep your codebase before each decision:
grep -rn "UPSTASH_REDIS" --include="*.ts" --include="*.tsx" --include="*.mjs" .
For 32blog.com, the grep told me Upstash was referenced only in two tutorial MDX files — the actual like-button feature had been removed months ago. That turned what looked like three rotations into two actual rotations plus a cleanup.
The Emergency Sequence I Ran
I did this within a couple of hours of logging in and seeing the badges. Total elapsed time: roughly 30 minutes including testing.
Step 1: Rotate the Resend API Key (First, Because Email Reputation)
A leaked Resend key lets an attacker send mail from @32blog.com with valid SPF/DKIM signatures. That's the worst-case scenario in this incident because domain reputation damage is slow to recover from. Phishing sent with your DKIM signature gets you onto Gmail's bulk-sender penalty lists for weeks.
- Logged into Resend → API Keys → revoked the old key
- Created a new API key, copied it once (Resend only shows it at creation time)
- Vercel → Settings → Environment Variables → deleted the old
RESEND_API_KEY - Clicked Add New, same key name
RESEND_API_KEY, pasted the new value - Checked the "Sensitive" box — this is the whole point of rotating
- Saved, then clicked Redeploy on the latest deployment
Step 2: Remove the Upstash Integration (Not Just the Variables)
Upstash was the unused-service case. Rather than rotate, I cleaned up:
- Logged into Upstash → deleted the Redis database and the entire account (no ongoing charges since I never added a card)
- Vercel → Integrations → Upstash → Remove Integration (this should auto-delete the env vars it provisioned)
- Verified
UPSTASH_REDIS_REST_URLandUPSTASH_REDIS_REST_TOKENwere gone from Environment Variables
This is cleaner than manually deleting the two env vars because the integration entry itself is gone — future reinstalls won't resurrect the old names.
Step 3: The Duplicate I Almost Left Behind
After the Resend re-add, I noticed my env var list showed two rows:
RESEND_API_KEY(Sensitive, Production and Preview, "Updated just now")RESEND_API_KEY_(trailing underscore, All Environments, "Updated 50s ago")
The trailing-underscore variant was a leftover from when I briefly kept the old key under a different name "in case I need to roll back." This is a bad pattern: the orphan row still contains the compromised value, and nothing references it. I deleted it.
Step 4: Proactively Sensitive-Flag the Other Secrets
GA_SERVICE_KEY_BASE64 had no rotation badge but is a Google service account private key. Same pattern as Resend:
- Google Cloud Console → IAM → Service Accounts → added a new JSON key
base64 -w 0 new-key.jsonto encode- Vercel → deleted the old
GA_SERVICE_KEY_BASE64→ re-added with Sensitive checked and the new base64 value - Redeploy → verified popular posts section still loads on the live site
- Google Cloud Console → deleted the old service account key
GA_PROPERTY_ID stayed as-is. A numeric property ID is already visible in client-side GA4 tags; it is not a secret.
What the Sensitive Flag Actually Buys You
Per Vercel's incident bulletin, Sensitive-flagged variables are stored in a way that "prevents them from being read." Operationally:
- Stored encrypted, decrypted only at build/runtime
- Cannot be viewed in the UI after save (only
●●●●●●is shown) - Cannot be edited in place, only replaced wholesale
- Cannot exist in Development environment — if you check "Sensitive," the Development checkbox greys out
That last constraint is by design: Development env vars are meant to be pulled down to your laptop by vercel dev, which is the opposite of what Sensitive means. Production + Preview is the only combination that works with Sensitive.
The Tradeoff
Once Sensitive, if you lose the value (forgot to save it in a password manager), your only recourse is to issue a new key from the provider. Vercel genuinely cannot show it to you. This is security working as designed, and it is the correct default for API keys — providers can always regenerate, so losing the value is recoverable.
The Gotcha with VERCEL_DEEP_CLONE
This one isn't security-related but caught me on the way through. VERCEL_DEEP_CLONE=true is not a standard documented Vercel variable, but it is read by Vercel's build infrastructure. Setting it changes the git clone from shallow (depth 10 by default) to a full clone.
Why that matters: my build script scripts/generate-updated-dates.mjs runs git log -1 --format=%aI -- <file> for each MDX article to populate the "last updated" date. Without a deep clone, git log returns empty for most files, and the updated-dates JSON silently becomes mostly empty.
If you have any build-time tooling that depends on git history — Nextra's "last updated" timestamps, Nx affected-graph for monorepos, changelog generators — you probably need VERCEL_DEEP_CLONE=true too. A GitHub discussion thread has the community reference. It's not a secret, leave it in plaintext.
Should You Consider Moving Off Vercel?
The obvious reaction to a platform breach is "time to self-host." Consider the tradeoff honestly:
- For: self-hosting on a VPS with Coolify eliminates this class of supply-chain risk — your secrets are on your own server, not a vendor's internal systems
- Against: you are now responsible for OS patching, TLS renewal, DDoS mitigation, and the Fluid Compute equivalents yourself. The attack surface moved, it didn't disappear.
My own position: 32blog.com stays on Vercel. The marginal security benefit of self-hosting is smaller than the marginal operational overhead for a one-person site. What I'm doing instead is the thing this article is about — treating Sensitive as the default for every new secret I add, and occasionally auditing the full env var list for plaintext regressions.
If you're running a larger team or storing genuinely high-value secrets (payment credentials, PII-access keys), the calculus shifts. Coolify on a $7/month VPS covers most Next.js deployment needs and puts the secret storage in your hands. The pain level is real but not prohibitive.
For cost-sensitive readers already worried about Vercel bills — this incident doesn't change that math, but the Spend Management guide is still the first stop before any self-hosting decision.
FAQ
How do I know if my Vercel env vars were compromised?
Check Settings → Environment Variables. Any row with an orange Need To Rotate badge was flagged by Vercel as part of the incident scope. No badge means Vercel's internal records don't associate that specific variable with the incident, but if it's stored without the Sensitive flag, treat it as a risk and migrate it to Sensitive on your next maintenance window.
Can I just keep using the old key if I don't see obvious abuse?
Technically yes, but dangerously. Key abuse from this class of breach often surfaces weeks or months later — via sudden Resend reputation drops, spam complaints on your domain, or unexplained usage-based billing spikes. The fixed cost of rotating now is small; the variable cost of waiting is large and unpredictable.
Why didn't Sensitive prevent this when it's supposedly the default?
It's not the default. Every variable I entered through the Vercel UI defaulted to "not sensitive," and every Marketplace integration (Upstash, Resend, Supabase, etc.) provisions vars in plaintext unless the integration explicitly supports Sensitive. Vercel has since stated they're reviewing the defaults. Until that changes, Sensitive is opt-in and you have to remember to check the box.
Does rotating in Vercel also invalidate the old key at the provider?
No. Vercel only stores the value — the provider (Resend, Upstash, Google Cloud) holds the actual key. You must explicitly revoke the old key in the provider's dashboard. Forgetting this step is the most common rotation mistake: you've updated Vercel but the old compromised key is still valid at the provider.
I rotated but the new key isn't working. What did I miss?
Almost always: you forgot to redeploy. Vercel environment variable changes do not propagate to existing deployments — they only apply to new builds. Go to Deployments, find the latest, click ⋯ → Redeploy. You can keep "Use existing Build Cache" checked since nothing in the build output changes.
Is VERCEL_DEEP_CLONE part of the incident?
No. It's a boolean flag (true/false), not a secret, and controls git clone depth during builds. If your build tooling reads git history, leave it set. If nothing in your build reads git, you can remove it with no impact.
Should I enable 2FA on my Vercel account now?
Yes. This incident came through a Vercel employee's compromised Workspace, not a customer account, but the general principle stands: Vercel → Settings → Security → enable 2FA, then review active sessions and revoke anything unfamiliar. It takes two minutes and closes one of the most common attack vectors against any SaaS account.
Wrapping Up
Three things worth doing right now, regardless of whether you saw a badge today:
- Audit your Vercel env var list for plaintext secrets. Any API key, database credential, or service account key that isn't Sensitive should be migrated. Resend/Upstash/Supabase integrations are the common culprits.
- Delete anything unused. Marketplace integrations from discontinued experiments are a long tail of dormant secrets — removing the integration cleans them up atomically.
- Adopt Sensitive as the default for every new variable you add from now on. It is slightly less convenient (you can't peek at the value later), but it is the only thing that demonstrably defended customers in this incident.
Supply-chain breaches against platform providers are rare, but they are not zero-frequency events. The defensive posture you can control — Sensitive flags, principle of least privilege, rotation discipline — is cheap to set up once and keeps working quietly after that.