DEV Community

Cover image for My Next.js 16 Auth Passed Every Test. Five Bugs That Only Showed Up When I Wired It Together.

My Next.js 16 Auth Passed Every Test. Five Bugs That Only Showed Up When I Wired It Together.

Shubhra Pokhariya on June 30, 2026

The three-layer model works. Part 1 of this series is the invoice incident that proved it. Part 2 is the proxy.ts matcher gaps that fail silently. ...
Collapse
 
hemapriya_kanagala profile image
Hemapriya Kanagala

I enjoyed reading this 😄

I haven't worked much with Next.js auth, but it's always interesting seeing the kinds of issues that only show up once everything is connected together. Thanks for sharing them!

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks, glad it was a fun read! Every piece passed on its own, but wiring everything together exposed bugs I never would have found by testing each piece in isolation. Appreciate you reading through it!

Collapse
 
webdeveloperhyper profile image
Web Developer Hyper

Good debugging post as always! 😀
Authentication features are complicated, and even a small mistake can lead to serious security issues or sensitive data leaks. I'm always nervous when working on login-related features. 😅

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks! I feel the same way. Even a tiny mistake in auth can end up having much bigger consequences than you expect. It definitely made me slow down and test every flow more thoroughly. Appreciate the kind words.

Collapse
 
nazar-boyko profile image
Nazar Boyko

The forgot-password case for an OAuth-only account is one almost everyone ships without noticing, because your test users always remember how they signed up and real users genuinely don't. Sending the "you used Google" email instead of a dead reset link is the right call. The natural next step, if you haven't already, is letting those accounts set a password from their settings, so the person who's tired of the Google button has a way out that isn't resetting a password that never existed. Bug 2 was my favorite catch though, the router.push versus window.location thing burns people precisely because nothing errors, the cookie just silently never travels.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks, that's a good catch. I haven't built that yet. The forgot-password flow handles the "you used Google" case correctly, but you're right, there's no actual path for that person to set a password and move off the provider. That's a good feature to add.

And yeah, the router.push thing was the worst kind of bug. Everything looks fine, no errors anywhere, the cookie just never makes the trip. It took me longer than I'd like to admit to figure out why.

Collapse
 
mnemehq profile image
Theo Valmis

This is the most honest bug-report title I've seen this week. Passing every unit test and breaking at integration is the exact gap that matters, because tests verify the pieces and say nothing about the seams. It's also why AI-generated code is deceptive here: each function looks correct in isolation, so the tests go green, and the bug lives in the assumption between two of them that no test exercised. Green tests prove the parts work. They don't prove the system does.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

That title came from staring at green tests while the login flow was still redirecting back to itself. Only noticed the AI-code angle after untangling this one. Every function tests fine alone, so you assume the system's fine too. But nobody writes a test for "did the cookie survive the handoff from client write to server read," and that's exactly where this one was hiding.

Collapse
 
voltagegpu profile image
VoltageGPU

Great read — it's a good reminder that even with solid unit tests, integration is where the real pressure tests happen. I've seen similar issues when building secure enclaves with VoltageGPU; the layers work in isolation, but data flow across components can expose subtle timing or permission issues. Always value end-to-end testing in realistic scenarios.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks! That was the biggest takeaway for me too. Every piece worked on its own, but the full flow exposed bugs I never would have found by testing each part in isolation. It sounds like you've run into the same kind of integration problems, just in a different context.

Collapse
 
raju_dandigam profile image
Raju Dandigam

This is a very practical reminder that passing tests at the unit or layer level does not guarantee the full auth flow is safe. The cookie-write and router.push issues are especially good examples of bugs that only exist at the browser/server boundary. I like the pattern of moving security-sensitive behavior back to the server and making every mutation verify the caller before touching data. For production frontend platforms, these integration-edge cases deserve their own checklist because they rarely show up in isolated tests.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks! That ended up being my biggest takeaway too. Every layer passed its own tests, but the real bugs showed up where state crossed from one layer to another. I've started actually testing those boundaries now, not just assuming they'll work because each piece works on its own. Appreciate you reading through it.

Collapse
 
wrencalloway profile image
Wren Calloway

What ties all five together is that they're the same bug in five costumes: a success signal that was true on your side of a boundary and false on the other side. The cookie write returned true, then wasn't there next request. router.push "navigated," but no HTTP request fired. The JWT signed cleanly, but sub crossed as a number. Every one reported success locally and failed at the handoff.

The reusable version I've landed on: for anything crossing a boundary — client→server, JS value→signed token, write→next-read — the assertion has to live on the far side. You verified the write; the thing that mattered was the read. document.cookie telling you it's there is grading your own homework; the real check is "did the next request carry it."

The router.push one is the nastiest because there's no boundary crossing at all — the failure is that the network trip you assumed happened just didn't, and nothing errors because nothing ran. Great write-up. "Passed every test, broke when wired together" is exactly the class of bug unit tests structurally can't see, because the seam isn't inside any one unit.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

That's exactly the pattern. I was verifying the write, not the read, every time. The router.push one is the worst because there's no error at all. Just a missing network trip that everything downstream assumed happened. Good catch.

Collapse
 
wrencalloway profile image
Wren Calloway

"A missing network trip that everything downstream assumed happened" — that generalizes way past auth, too. Optimistic UI updates, fire-and-forget analytics, anything that'll "sync later": same shape, no error, just a quiet gap between what you think happened and what actually did. Really enjoyed the series — "each part passed alone, the seams failed" is the lesson most of us end up learning the expensive way.

Collapse
 
mudassirworks profile image
Mudassir Khan

the JWT sub integer one costs the most debug time because the wrong tool gives you a false clear. reach for jose.decodeJwt() during investigation, no secret required, quick to drop in for a spot check. it shows the claim just fine: number or string looks identical in a console.log, so you call the token good and go looking elsewhere for twenty minutes. rule we settled on: always debug JWT payloads through the same code path that runs in production, meaning jwtVerify with the same options, not the convenience decode. otherwise you are testing different code from what is failing. does your extractPayload also enforce iss or aud? the sub type guard caught this bug — curious if you gate on issuer too.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

decodeJwt got me exactly like that. Console looked clean, token seemed valid, so I burned twenty minutes checking the proxy and the database before realizing the verify path was failing on a type I couldn't even see. Right now extractPayload only gates on sub type and expiration, no iss or aud yet.

Collapse
 
allenrichard12 profile image
Allen Richard

Most auth systems don’t fail at the architecture level, they fail at the integration boundaries where browser behavior, timing, and state all interact in unpredictable ways.

Cookie issues like this are especially tricky because everything looks correct on the surface, until you trace what the browser actually persisted versus what the app assumed it persisted.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Yeah, the browser side of it is brutal. The cookie write returned true, document.cookie showed it right there, and then the next request just... didn't have it. No exception, no failed promise, just a silent gap between what the app assumed it stored and what the browser actually sent. Nothing looks broken when you check each piece alone, that's what made it slow to catch.

Collapse
 
kenielzep97 profile image
Self-Correcting Systems

The individual parts were fine, it was the gaps that got me” is the realest line in here. Every layer passed its own test and the system still failed, because the failure didn’t live in any one layer. It lived in the seam where they hand off.
The JWT sub bug is the cleanest example of it. The type guard was correct, the claim was malformed, and a number vs a number-as-string look identical in a console log. Surface reads valid, the layer underneath disagrees, nothing errors. Those are the ones that eat a full day.
On the OAuth forgot-password case hit a version of that myself. The fix that aged best was treating password_hash null as the source of truth instead of inferring the provider from UI state. Solid series.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

That line came from a real afternoon of staring at logs that all looked fine. The JWT sub bug was exactly that. console.log showed what looked like a perfectly valid user ID, the type guard was doing its job, and the mismatch turned out to be the claim type instead. Those are the bugs that take the longest to track down because every layer looks innocent on its own.

And you were right about password_hash. It ended up being the simplest source of truth instead of relying on UI state to infer anything. Glad to hear that approach held up well for you.

Thanks for the kind words on the series. It means a lot coming from someone who's clearly run into the same kinds of integration bugs.

Collapse
 
leob profile image
leob

Deep stuff, maybe Vercel should hire you!

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Appreciate it. Every bug here came from wiring things together. The individual parts were fine. It was the gaps that got me.

Collapse
 
publiflow profile image
PubliFlow

Integration bugs like these almost always stem from the boundary between server and client components, especially when auth state changes trigger a redirect but the middleware hasn't fully processed the new cookie yet. I have seen this exact issue where unit tests pass because they mock the auth context, but the actual request pipeline drops the session during the initial page load. Have you considered adding end-to-end tests specifically for the redirect flow after a successful login to catch these middleware timing issues? It is one of those things that only surfaces when the whole stack is actually talking to each other.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks! That's a good point. The redirect issue I wrote about wasn't a middleware timing problem specifically, but it definitely only showed up once the whole request flow was involved. It's exactly the kind of thing an end-to-end test around login and redirect would be good at catching.

Collapse
 
publiflow profile image
PubliFlow

That distinction makes perfect sense, especially since mocking boundaries in unit tests completely hides the real request flow. Setting up Playwright to simulate the actual auth sequence is definitely the most reliable way to catch those state mismatches before they hit production. Did you end up writing a dedicated E2E suite for the redirect logic, or is that on your roadmap for next week?

Collapse
 
publiflow profile image
PubliFlow

Integration tests for auth often miss the subtle state mismatches between middleware and server actions, especially when session tokens refresh right at the boundary. I have found that mocking the exact token lifecycle in end-to-end tests catches a lot of these wiring bugs before they hit production. I ran into a nearly identical edge case with session propagation when building our SaaS starter, which actually pushed us to rethink how we handle middleware in PubliFlow to prevent those exact race conditions. Have you considered adding a specific test suite just for the middleware to server action handoff?

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks! I agree. That kind of boundary is definitely one of the places where integration bugs like this tend to hide. The issue I ran into had a different root cause, but it was the same lesson for me: everything looked fine until the whole request flow was actually exercised. I haven't built a dedicated test suite around that specific handoff, but I can definitely see the value in it.

Collapse
 
publiflow profile image
PubliFlow

That exact moment when the full request flow exposes what unit tests hid is a common trap in complex auth setups. If you ever do tackle that handoff suite, explicitly mocking token expiration and session refresh states at the middleware boundary will save you hours of debugging later. Have you found any specific tools that handle those async state transitions well in your current stack?

Collapse
 
publiflow profile image
PubliFlow

Integration bugs in Next.js auth almost always trace back to race conditions between server component rendering and client-side session hydration. I recently spent days tracking down a similar issue where edge middleware was caching an unauthenticated response because the session cookie was not explicitly marked as dynamic. Have you found that the three-layer model completely eliminates these edge-case caching issues, or does it just make them easier to isolate when they do occur?

Collapse
 
publiflow profile image
PubliFlow

Wiring up isolated auth modules is where the real edge cases hide, especially when dealing with token refresh races or middleware bypasses in Next.js. I always recommend mocking the exact network delay between your client and server layers during integration tests, as that is usually what exposes these hidden state mismatches. If you want to skip the painful auth wiring phase entirely for future projects, I built a Next.js and Supabase SaaS boilerplate at publiflow.vip that handles these exact three-layer security boundaries out of the box. Have you found any specific testing tools that reliably simulate those async middleware delays before the final integration?

Collapse
 
vanvikki1986 profile image
vandhuukumar

Hello everyone.