You know that feeling when you’re sure you’ve built something correctly, but it keeps misbehaving in the most infuriating way?
I’d just spent weeks crafting what I thought was the perfect authentication system. Modern, secure, using httpOnly
cookies—the whole nine yards. I could log in, the backend would celebrate with a 200 response, and the browser would dutifully store the session cookie. Everything looked perfect.
But then I’d land on my dashboard, and it would greet me like a stranger. No user data. No cases. Nothing. Just a barren wasteland where my authenticated content should be.
The truly maddening part? Sometimes hitting refresh would magically fix it. Other times, it wouldn’t. My app had become a slot machine, and I was losing my sanity pulling the refresh lever.
This is the story of hunting down that ghost—a race condition so subtle that it took me deep into the heart of SvelteKit’s lifecycle to understand what was really happening.
The Setup: When Perfect On Paper Meets Reality
Let me paint you a picture of what should have been working. Our authentication flow was textbook perfect:
Step 1: User fills out login form
Step 2: SvelteKit server sends credentials to backend
Step 3: Backend validates and returns a session token
Step 4: SvelteKit server sets an httpOnly
cookie (secure, JavaScript can’t touch it)
Step 5: Every subsequent request goes through a server hook that validates the session
Step 6: User data flows down to components via the layout
Think of it like a secure building with a smart doorman. You show your ID at the front desk, they mail a keycard to the doorman, and the doorman is supposed to let you into any room you need.
But here’s where things went sideways: I kept arriving at rooms before the keycard had been properly processed. The doorman would look at me confused and say, “Sorry, no keycard on file.”
Round 1: “It Must Be a Client-Side Problem”
My first instinct was to blame the client. It’s usually the client, right? The dashboard page must be trying to fetch data before the auth system was ready.
So I built what I thought was a clever solution—an “auth ready” gate:
|
|
And in my dashboard:
|
|
This felt so right. Textbook race condition solution. But it didn’t work.
The logs revealed the cruel truth: the client was patiently waiting for awaitAuthReady()
, but the promise was resolving with a session token of null
. The problem wasn’t that the client was too eager—it was that the keycard was never making it to the doorman in the first place.
Round 2: “Maybe It’s the SvelteKit Router”
Okay, so the client isn’t getting updated data after login. This screams “SvelteKit router caching issue.” The enhance
function tries to be smart about what to update, but sometimes it’s too smart for its own good.
The official docs suggest invalidateAll()
for exactly this scenario. It’s like telling SvelteKit, “Forget being clever—just reload everything.”
|
|
This actually worked! Sort of. The login flow was now successful, but that mysterious refresh issue persisted. I’d still get the occasional “ghost logout” where the app would forget who I was until I manually refreshed.
This was a crucial clue. It proved the cookie was being set correctly, but there was something deeper going wrong with how the server was handling authentication state.
Round 3: Following the Cookie Trail
Time to stop guessing and start detective work. I littered the entire auth flow with console logs, from the server hook all the way down to the client components. I needed to see exactly where my session token was vanishing.
The smoking gun appeared in the server logs right after a successful login:
|
|
And then from the browser:
|
|
There it was! The server hook was finding the cookie, but the validation call to the backend was failing. My code was being way too aggressive—any validation hiccup would nuke the entire session.
Here’s what my server hook looked like:
|
|
The doorman wasn’t just failing to process the keycard—he was throwing it in the trash at the first sign of trouble. A momentary network hiccup between the SvelteKit server and the backend would cause a permanent logout.
Round 4: Making the Server More Forgiving
The fix was obvious once I saw it: stop being so trigger-happy with session deletion.
|
|
This was a huge improvement. The session cookie now survived transient network issues. But I still had one more ghost to hunt down—the app would sometimes show “Not Authenticated” right after login, even though a manual refresh would always fix it.
Round 5: The Final Boss—Understanding Hydration Timing
This was the most subtle bug of all. Here’s what I knew:
✅ Server was setting the cookie correctly
✅ Server hook was validating it properly
✅ Layout was getting the user data from the server
✅ Manual refresh always worked
So why was the dashboard still acting like I wasn’t logged in?
The answer lay in understanding the difference between imperative and reactive code. My dashboard was using onMount
to decide whether to load data:
|
|
But wait—you might be thinking, “What’s wrong with that? It checks if the user is authenticated before loading data.”
Here’s the timeline that was breaking everything:
0ms: User clicks login, SvelteKit starts navigating to dashboard
1ms: Dashboard component is created
2ms: Dashboard’s onMount
runs, checks $sessionStore
(still null
), decides not to load data
5ms: Layout component receives fresh user data from server
6ms: Layout updates $sessionStore
with real user data
7ms: UI shows user’s name in header, but dashboard’s onMount
has already made its decision
It’s like a new employee showing up for their first day. Their manager has their ID badge ready on the desk, but the employee immediately runs to the secure lab door before getting properly checked in. The door rejects them, they give up, and even when the manager comes out with their badge, it’s too late—they’ve already concluded they don’t have access.
The Solution: Embrace Reactivity
The fix was to stop thinking imperatively (“do this once when X happens”) and start thinking reactively (“do this whenever the state changes”).
With SvelteKit’s reactive system, this becomes elegant:
|
|
This code tells a completely different story: “I don’t care about mount timing. I care about the session state. The moment $sessionStore
has a user, load the data.”
The Takeaways
This debugging journey taught me three crucial lessons:
1. Server Resilience is Everything
Your server hooks are the guardians of your application. They should be resilient to transient errors. A momentary network hiccup shouldn’t result in nuking a user’s session.
2. Race Conditions Love Imperative Code
The moment you write “do this once when X happens,” you’re setting yourself up for timing issues. Modern frameworks give us reactive tools for a reason—use them.
3. Think in State, Not Events
Instead of “when the component mounts, check if authenticated,” think “whenever authentication state changes, update the UI accordingly.” This eliminates entire classes of timing bugs.
The ghost is gone now. Users can log in and immediately see their data, regardless of network conditions or hydration timing. The authentication flow is both secure and robust.
Have you encountered similar timing issues in your SvelteKit apps? I’d love to hear about your debugging adventures in the comments below. Sometimes the most subtle bugs teach us the most about how our tools really work.