Authentication Is the Most Important and Critical Part of Your Stack
Let’s get one thing straight before we start: cookies aren’t the “old” way of doing auth that JWT came to replace. Cookies are a browser feature. They were built into HTTP so the browser could carry a little piece of state for you, automatically, on every request no extra code, no extra fetch, no extra thinking.
That’s the whole superpower. You set a cookie once, and the browser attaches it to every future request to your domain, all by itself. No manually reading a token, sticking it in an Authorization header, remembering to do it on every single API call. The browser just... does it. That’s why cookies became the default vehicle for session and auth data in the first place not because nothing better existed, but because “auto-attach on every request” is genuinely hard to beat for convenience.
Of course, “the browser sends this automatically, everywhere, to this domain” is also exactly the property that makes cookies dangerous if you don’t lock them down. Which brings us to the actual story here.
The Problem Cookies Created for Themselves: CSRF
Picture this. You’re logged into your bank in one tab. In another tab, you land on some sketchy site that has a hidden form silently submitting a POST request to yourbank.com/transfer-funds. Your browser, being helpful as always, attaches your bank’s session cookie to that request because that’s just what cookies do, cross-site or not, by default. The bank’s server sees a valid cookie and processes the transfer. You never clicked anything on the bank’s site.
That’s CSRF (Cross-Site Request Forgery) and it exists specifically because cookies auto-attach. The same feature that makes cookies smart is the same feature that makes them exploitable.
For years, the fix was clunky: anti-CSRF tokens embedded in every form, validated server-side, invisible to the user, annoying to implement everywhere.
Then browsers shipped a much cleaner fix directly into the cookie itself: SameSite.
SameSite: The Wins, the Fails, and the Comeback
SameSite is an attribute you set on the cookie that tells the browser: when should you actually send this thing cross-site? There are three settings, and each one has a personality.
SameSite=None the fail. This is the old default behavior, made explicit. Send the cookie on literally every request, same-site or cross-site, no questions asked. This is what let the bank-transfer CSRF scenario happen. It’s still useful for genuine cross-site needs OAuth handshakes, embedded widgets, third-party iframes but it must be paired with Secure, or modern browsers just silently drop the cookie.
SameSite=Strict the overcorrection. Never send the cookie cross-site. Period. Sounds airtight and it is, for CSRF. But here’s the fail nobody mentions until it bites them: if a user clicks a link to your site from an email, a Slack message, or a Google search result, that first request is technically cross-site. With Strict, your cookie won’t ride along, so the user lands on your site looking logged out even though they’re not. Confusing, and it tanks conversion on anything you’d want people clicking into from outside (marketing links, shared links, password reset emails).
SameSite=Lax the comeback. This is the one that actually won. Lax sends the cookie on cross-site top-level navigations the kind that happens when someone clicks a link and it loads in the address bar as long as it’s a GET request. It blocks the cookie on cross-site POSTs, AJAX calls, iframes, and background requests. That one distinction kills basically every real CSRF payload (which almost always relies on a POST or a background request) while keeping the experience smooth for normal browsing.
Get Jainananya’s stories in your inbox
Join Medium for free to get updates from this writer.
This is your referrer/redirect case, spelled out: someone clicks a link from an email or a Slack DM pointing straight at your dashboard. That’s a cross-site, top-level, GET navigation. Under Lax, the cookie comes along for the ride, and they land already logged in. Under Strict, they’d hit a logged-out page and have to re-auth for no good reason. Lax is the setting that respects how people actually arrive at your site through links, not by typing your URL from scratch every time.
That’s why Lax is the default modern browsers apply if you don’t set anything at all, and why it’s the recommended baseline for session cookies almost everywhere you look. Save Strict for something narrower and higher-stakes, like a refresh-token cookie scoped only to your /auth routes somewhere the “logged out after clicking a link” tradeoff doesn’t matter because nobody’s linking directly into it anyway.
One caveat worth tattooing on your arm: Lax protects GETs, not the actions behind them. Never put a state-changing action behind a GET request (”delete my account” as a link you can click) that sidesteps the whole protection. Keep mutations on POST/PUT/DELETE, pair with httpOnly + Secure, and SameSite is doing real work for you.
Does Next.js Make Cookies Actually Better?
In a pure client-rendered React app, the browser has your cookie, sure but your React code doesn’t know the user is logged in until it runs some JS, fires a request (usually to something like /api/me), and waits for a response. That’s an extra round trip, every time, before your UI can confidently render “Welcome back” instead of a loading spinner.
In Next.js with server rendering, the cookie is already sitting in the incoming HTTP request before your page ever renders. Your Server Component can call cookies() from next/headers, read the session cookie, verify it, and render the fully personalized page all in the same request-response cycle that loaded the page in the first place. No separate client-side fetch needed just to establish “who is this.”
So yes in this specific sense, cookies genuinely pull more weight in Next.js than in plain CSR React. It’s not that cookies work differently there; it’s that SSR lets you use the cookie the moment it arrives, instead of needing a follow-up call once the page is already loaded in the browser. That’s a real reduction in network chatter, and it’s also why the logged-in state can appear instantly with no flash of “loading…” the personalization already happened server-side before any HTML reached the browser.
The one thing that hasn’t changed regardless of CSR or SSR: your Client Components still can’t touch an httpOnly cookie directly, by design. If a client-side button needs to know who’s logged in, it still gets that through a server-rendered prop or a small /api/me call the cookie itself stays exactly as invisible to JavaScript as it’s supposed to be.
The Cheat Sheet
Setting Sent cross-site on… Best for Strict Nothing cross-site, ever High-stakes, narrowly-scoped cookies (e.g. /auth refresh token) Lax Top-level GET navigations only (link clicks) General session/auth cookies the default for a reason None Everything, cross-site included (needs Secure) OAuth handshakes, embedded widgets, legit third-party use
And the one-line summary: cookies aren’t a legacy fallback they’re the browser doing your auth bookkeeping. CSRF was the cost of that convenience, SameSite=Lax is the fix that kept the convenience while killing the cost, and Next.js just lets you cash in on that convenience one request earlier than plain React can.
Why Teams Quietly Go Back to JWT in a Header
This is the point where a lot of teams stop fighting the cookie and just switch mechanisms. Instead of relying on the browser to attach a cookie across domains which Safari and Firefox may simply refuse to do they issue a JWT and have the frontend attach it manually:
Authorization: Bearer <jwt>Your frontend code reads the token (usually kept in memory, not localStorage) and explicitly sets that header on every API call. Because you’re doing the attaching yourself, in your own JavaScript, none of the browser’s cross-site cookie policies apply. Chrome, Safari, Firefox doesn’t matter. If your code sends the header, the header gets sent.
You do give something up here, and it’s worth naming honestly: you lose the “browser does it for free” convenience that made cookies so appealing in the first place. Now your frontend is responsible for storing the token safely, attaching it correctly on every request, and handling refresh logic that a cookie-based flow got for nothing. It’s more code, and if that token ends up in localStorage instead of memory, you’ve reopened the door to XSS-based token theft the exact risk httpOnly cookies existed to close.
But for the specific problem of multiple, unrelated frontend domains talking to one API, this trade is usually worth it. You’re exchanging “convenient but silently broken for a chunk of Safari/Firefox users” for “more code to write, but it actually works the same way everywhere.”






