what we were building

The webinar analytics dashboard (an internal Next.js 16 app for an ecommerce business that turns Zoom + Shopify CSV exports into post-webinar performance reports) needed a share feature.

The use case: after each webinar, we want to send the KOL doctor a link to their own metrics without giving them an admin login.

So I built a custom share layer — /share/[token] with an email gate, JWT-signed cookies, the works.

The signing code lives in src/lib/share-auth.ts. The getSecret() function there tries to find a secret to sign and verify those cookies. It uses a dedicated SHARE_SESSION_SECRET if one is set, and falls back to NextAuth's own secret if it isn't.

Clean design. One less env var to manage. Reuse what's already required for auth.

the trap

Here's what I wrote for the fallback:

const secret = process.env.SHARE_SESSION_SECRET || process.env.NEXTAUTH_SECRET;

That's a perfectly reasonable line of code — if you're on NextAuth v4.

This app runs NextAuth v5 (Auth.js, the rebranded release). And v5 renamed the environment variable.

It's not NEXTAUTH_SECRET anymore. It's AUTH_SECRET.

My .env.local had AUTH_SECRET set (because that's what the Auth.js docs tell you to set). Vercel had AUTH_SECRET set. Nobody had NEXTAUTH_SECRET set anywhere.

So in production, process.env.NEXTAUTH_SECRET resolved to undefined.

The fallback was dead. Silently, completely dead.

why it was hard to catch

This is the sneaky part. It didn't throw at startup. It didn't log a warning. It threw at the moment a share link was verified — deep inside a route handler, wrapped in a try/catch, returning a generic 401.

Share links just... didn't work in production. The feature felt broken. And the error message wasn't helpful: SHARE_SESSION_SECRET (or NEXTAUTH_SECRET fallback) must be set. If you don't already know that NEXTAUTH_SECRET is a v4 name, that error sends you down a completely wrong path (do I need to add another env var?).

I would have had to go to Vercel, add SHARE_SESSION_SECRET, redeploy — and that would've fixed the symptom while completely missing the cause.

the fix

One line:

const secret = process.env.SHARE_SESSION_SECRET || process.env.AUTH_SECRET;

That's it. NEXTAUTH_SECRETAUTH_SECRET. The fallback now resolves to the secret that's actually in the environment. Share links work. No extra Vercel env var required.

why this matters

If you're building any custom layer that piggybacks on NextAuth's config secret — cron protection, webhook verification, share tokens, anything — you're probably writing NEXTAUTH_SECRET from muscle memory.

That's wrong if you're on v5.

The gotcha is that it can pass local testing completely (especially if your .env.local happened to define both names at some point), then silently fail in production where only AUTH_SECRET exists.

The rule I'm internalizing: NextAuth v5 / Auth.js uses AUTH_SECRET. Grep your codebase for NEXTAUTH_SECRET before you ship. Any reference outside a comment is probably a v4 holdover that will blow up exactly when you don't want it to — at 11pm, after you've sent the share link to a doctor.

P.S. While I was in there I also did a full forbidden-field audit of the share payload — recipientEmail never leaves the server, robots noindex on the share route, generic error messages on the email gate. If you're building shareable links into an internal tool, those are the other three things worth checking before you call it done.