[{"data":1,"prerenderedAt":1224},["ShallowReactive",2],{"layout-sidebar-\u002Fblog\u002Fnuxt-server-side-auth":3,"blog-nuxt-server-side-auth":38},{"type":4,"author":5,"date":6,"status":7,"toc":8},"blog-detail","Fabian Kirchhoff","2026-01-15","published",[9,13,16,19,23,26,29,32,35],{"id":10,"text":11,"depth":12},"the-problem","The Problem",2,{"id":14,"text":15,"depth":12},"the-nuxt-lifecycle","The Nuxt Lifecycle",{"id":17,"text":18,"depth":12},"the-new-architecture","The New Architecture",{"id":20,"text":21,"depth":22},"_1-session-guardts-nitro-server-middleware","1. session-guard.ts — Nitro server middleware",3,{"id":24,"text":25,"depth":22},"_2-authstorets-client-side-source-of-truth","2. authStore.ts — client-side source of truth",{"id":27,"text":28,"depth":22},"_3-useauthts-component-api","3. useAuth.ts — component API",{"id":30,"text":31,"depth":12},"results","Results",{"id":33,"text":34,"depth":12},"what-moved-where","What Moved Where",{"id":36,"text":37,"depth":12},"key-takeaway","Key Takeaway",{"id":39,"title":40,"author":5,"body":41,"date":6,"description":1213,"extension":1214,"featured":288,"meta":1215,"navigation":288,"order":12,"path":1216,"seo":1217,"specs":1218,"status":7,"stem":1222,"tag":1219,"__hash__":1223},"blog\u002Fblog\u002Fnuxt-server-side-auth.md","Server-Side Auth in Nuxt: From 4 Redirects to Zero",{"type":42,"value":43,"toc":1201},"minimark",[44,48,61,64,67,75,85,92,100,102,105,110,138,143,161,164,166,169,177,180,596,599,619,626,629,886,893,896,1077,1079,1082,1088,1094,1097,1102,1108,1111,1167,1169,1180,1189,1191,1194,1197],[45,46,40],"h1",{"id":47},"server-side-auth-in-nuxt-from-4-redirects-to-zero",[49,50,51,52,56,57,60],"p",{},"Every page load in our admin app went through four redirects before the user saw anything. Not just on login — on ",[53,54,55],"em",{},"every"," request, for ",[53,58,59],{},"already authenticated"," users. The fix required understanding exactly when Nuxt runs code and moving auth to the right place in the lifecycle.",[62,63,11],"h2",{"id":10},[49,65,66],{},"Our initial auth implementation used a Nuxt route middleware — the standard approach you'll find in most tutorials. The middleware checked for a valid session and redirected to the OAuth provider if none was found.",[49,68,69,70,74],{},"The redirect chain for an authenticated user hitting ",[71,72,73],"code",{},"\u002Fadmin\u002Fwelcome",":",[76,77,82],"pre",{"className":78,"code":80,"language":81},[79],"language-text","\u002Fadmin\u002Fwelcome\n  → \u002Fadmin\u002Flogin          (middleware: no session found)\n  → cognitor.test\u002Foauth\u002Fauthorize  (login page redirects to OAuth)\n  → \u002Fadmin\u002Flogin\u002Fcallback (OAuth redirects back with code)\n  → \u002Fadmin\u002Fwelcome        (callback sets session, redirects to destination)\n","text",[71,83,80],{"__ignoreMap":84},"",[49,86,87,88,91],{},"Four round-trips. Over 2 seconds of total duration. And this happened on ",[53,89,90],{},"every page load"," — not just on first login.",[49,93,94,95,99],{},"The root cause: ",[96,97,98],"strong",{},"Nuxt route middleware runs on the client, after the Vue app has already booted",". By the time the middleware checked for a session, the browser had already loaded HTML, parsed JS, and hydrated the app. A redirect at that point means doing all of it again.",[62,101,15],{"id":14},[49,103,104],{},"To understand the fix, you need to know when code actually runs:",[49,106,107],{},[96,108,109],{},"Server (on every request):",[111,112,113,117,123,126,129,132,135],"ol",{},[114,115,116],"li",{},"Nitro server plugins",[114,118,119,122],{},[96,120,121],{},"Nitro server middleware"," ← fastest possible interception",[114,124,125],{},"Nuxt app plugins",[114,127,128],{},"Route validation",[114,130,131],{},"Nuxt route middleware ← where our old auth lived",[114,133,134],{},"Render page",[114,136,137],{},"Send HTML",[49,139,140],{},[96,141,142],{},"Client (in browser):",[111,144,145,148,150,152,155,158],{},[114,146,147],{},"Parse HTML",[114,149,125],{},[114,151,128],{},[114,153,154],{},"Nuxt route middleware",[114,156,157],{},"Hydration",[114,159,160],{},"Page is interactive",[49,162,163],{},"The old auth ran at step 5 on the server, then again at step 4 on the client — after all the heavy lifting. Moving it to step 2 means the redirect happens before a single byte of Vue is processed.",[62,165,18],{"id":17},[49,167,168],{},"Three components with clear separation of concerns:",[170,171,172,173,176],"h3",{"id":20},"1. ",[71,174,175],{},"session-guard.ts"," — Nitro server middleware",[49,178,179],{},"The gatekeeper. Runs on every request before the Nuxt app boots:",[76,181,185],{"className":182,"code":183,"language":184,"meta":84,"style":84},"language-typescript shiki shiki-themes github-dark github-dark","\u002F\u002F server\u002Fmiddleware\u002Fsession-guard.ts\nexport default defineEventHandler(async (event) => {\n  const path = getRequestURL(event).pathname\n  const isPublic = publicRoutes.some(route => path.startsWith(route))\n\n  if (isPublic) return\n\n  \u002F\u002F Handle OAuth callback — no token needed here\n  if (path.startsWith('\u002Flogin\u002Fcallback')) {\n    const { code, state } = getQuery(event)\n    const tokens = await exchangeCodeForTokens(code, state)\n    setAuthCookies(event, tokens)\n    return sendRedirect(event, '\u002F')\n  }\n\n  \u002F\u002F Validate existing session\n  const accessToken = getCookie(event, 'access_token')\n\n  if (!accessToken) {\n    return sendRedirect(event, buildOAuthUrl(event))\n  }\n\n  const isValid = await validateToken(accessToken)\n\n  if (!isValid) {\n    const refreshed = await refreshAccessToken(event)\n    if (!refreshed) return sendRedirect(event, buildOAuthUrl(event))\n  }\n\n  \u002F\u002F Pass token to client via SSR context\n  event.context.auth = { accessToken }\n})\n","typescript",[71,186,187,196,232,250,283,290,302,307,313,332,361,380,389,407,413,418,424,444,449,462,477,482,487,505,510,522,539,563,568,573,579,590],{"__ignoreMap":84},[188,189,192],"span",{"class":190,"line":191},"line",1,[188,193,195],{"class":194},"sJ8bj","\u002F\u002F server\u002Fmiddleware\u002Fsession-guard.ts\n",[188,197,198,202,205,209,213,216,219,223,226,229],{"class":190,"line":12},[188,199,201],{"class":200},"sOPea","export",[188,203,204],{"class":200}," default",[188,206,208],{"class":207},"sFR8T"," defineEventHandler",[188,210,212],{"class":211},"suv1-","(",[188,214,215],{"class":200},"async",[188,217,218],{"class":211}," (",[188,220,222],{"class":221},"s-3mD","event",[188,224,225],{"class":211},") ",[188,227,228],{"class":200},"=>",[188,230,231],{"class":211}," {\n",[188,233,234,237,241,244,247],{"class":190,"line":22},[188,235,236],{"class":200},"  const",[188,238,240],{"class":239},"s8ozJ"," path",[188,242,243],{"class":200}," =",[188,245,246],{"class":207}," getRequestURL",[188,248,249],{"class":211},"(event).pathname\n",[188,251,253,255,258,260,263,266,268,271,274,277,280],{"class":190,"line":252},4,[188,254,236],{"class":200},[188,256,257],{"class":239}," isPublic",[188,259,243],{"class":200},[188,261,262],{"class":211}," publicRoutes.",[188,264,265],{"class":207},"some",[188,267,212],{"class":211},[188,269,270],{"class":221},"route",[188,272,273],{"class":200}," =>",[188,275,276],{"class":211}," path.",[188,278,279],{"class":207},"startsWith",[188,281,282],{"class":211},"(route))\n",[188,284,286],{"class":190,"line":285},5,[188,287,289],{"emptyLinePlaceholder":288},true,"\n",[188,291,293,296,299],{"class":190,"line":292},6,[188,294,295],{"class":200},"  if",[188,297,298],{"class":211}," (isPublic) ",[188,300,301],{"class":200},"return\n",[188,303,305],{"class":190,"line":304},7,[188,306,289],{"emptyLinePlaceholder":288},[188,308,310],{"class":190,"line":309},8,[188,311,312],{"class":194},"  \u002F\u002F Handle OAuth callback — no token needed here\n",[188,314,316,318,321,323,325,329],{"class":190,"line":315},9,[188,317,295],{"class":200},[188,319,320],{"class":211}," (path.",[188,322,279],{"class":207},[188,324,212],{"class":211},[188,326,328],{"class":327},"s4wv1","'\u002Flogin\u002Fcallback'",[188,330,331],{"class":211},")) {\n",[188,333,335,338,341,343,346,349,352,355,358],{"class":190,"line":334},10,[188,336,337],{"class":200},"    const",[188,339,340],{"class":211}," { ",[188,342,71],{"class":239},[188,344,345],{"class":211},", ",[188,347,348],{"class":239},"state",[188,350,351],{"class":211}," } ",[188,353,354],{"class":200},"=",[188,356,357],{"class":207}," getQuery",[188,359,360],{"class":211},"(event)\n",[188,362,364,366,369,371,374,377],{"class":190,"line":363},11,[188,365,337],{"class":200},[188,367,368],{"class":239}," tokens",[188,370,243],{"class":200},[188,372,373],{"class":200}," await",[188,375,376],{"class":207}," exchangeCodeForTokens",[188,378,379],{"class":211},"(code, state)\n",[188,381,383,386],{"class":190,"line":382},12,[188,384,385],{"class":207},"    setAuthCookies",[188,387,388],{"class":211},"(event, tokens)\n",[188,390,392,395,398,401,404],{"class":190,"line":391},13,[188,393,394],{"class":200},"    return",[188,396,397],{"class":207}," sendRedirect",[188,399,400],{"class":211},"(event, ",[188,402,403],{"class":327},"'\u002F'",[188,405,406],{"class":211},")\n",[188,408,410],{"class":190,"line":409},14,[188,411,412],{"class":211},"  }\n",[188,414,416],{"class":190,"line":415},15,[188,417,289],{"emptyLinePlaceholder":288},[188,419,421],{"class":190,"line":420},16,[188,422,423],{"class":194},"  \u002F\u002F Validate existing session\n",[188,425,427,429,432,434,437,439,442],{"class":190,"line":426},17,[188,428,236],{"class":200},[188,430,431],{"class":239}," accessToken",[188,433,243],{"class":200},[188,435,436],{"class":207}," getCookie",[188,438,400],{"class":211},[188,440,441],{"class":327},"'access_token'",[188,443,406],{"class":211},[188,445,447],{"class":190,"line":446},18,[188,448,289],{"emptyLinePlaceholder":288},[188,450,452,454,456,459],{"class":190,"line":451},19,[188,453,295],{"class":200},[188,455,218],{"class":211},[188,457,458],{"class":200},"!",[188,460,461],{"class":211},"accessToken) {\n",[188,463,465,467,469,471,474],{"class":190,"line":464},20,[188,466,394],{"class":200},[188,468,397],{"class":207},[188,470,400],{"class":211},[188,472,473],{"class":207},"buildOAuthUrl",[188,475,476],{"class":211},"(event))\n",[188,478,480],{"class":190,"line":479},21,[188,481,412],{"class":211},[188,483,485],{"class":190,"line":484},22,[188,486,289],{"emptyLinePlaceholder":288},[188,488,490,492,495,497,499,502],{"class":190,"line":489},23,[188,491,236],{"class":200},[188,493,494],{"class":239}," isValid",[188,496,243],{"class":200},[188,498,373],{"class":200},[188,500,501],{"class":207}," validateToken",[188,503,504],{"class":211},"(accessToken)\n",[188,506,508],{"class":190,"line":507},24,[188,509,289],{"emptyLinePlaceholder":288},[188,511,513,515,517,519],{"class":190,"line":512},25,[188,514,295],{"class":200},[188,516,218],{"class":211},[188,518,458],{"class":200},[188,520,521],{"class":211},"isValid) {\n",[188,523,525,527,530,532,534,537],{"class":190,"line":524},26,[188,526,337],{"class":200},[188,528,529],{"class":239}," refreshed",[188,531,243],{"class":200},[188,533,373],{"class":200},[188,535,536],{"class":207}," refreshAccessToken",[188,538,360],{"class":211},[188,540,542,545,547,549,552,555,557,559,561],{"class":190,"line":541},27,[188,543,544],{"class":200},"    if",[188,546,218],{"class":211},[188,548,458],{"class":200},[188,550,551],{"class":211},"refreshed) ",[188,553,554],{"class":200},"return",[188,556,397],{"class":207},[188,558,400],{"class":211},[188,560,473],{"class":207},[188,562,476],{"class":211},[188,564,566],{"class":190,"line":565},28,[188,567,412],{"class":211},[188,569,571],{"class":190,"line":570},29,[188,572,289],{"emptyLinePlaceholder":288},[188,574,576],{"class":190,"line":575},30,[188,577,578],{"class":194},"  \u002F\u002F Pass token to client via SSR context\n",[188,580,582,585,587],{"class":190,"line":581},31,[188,583,584],{"class":211},"  event.context.auth ",[188,586,354],{"class":200},[188,588,589],{"class":211}," { accessToken }\n",[188,591,593],{"class":190,"line":592},32,[188,594,595],{"class":211},"})\n",[49,597,598],{},"Key points:",[600,601,602,609,612],"ul",{},[114,603,604,605,608],{},"The OAuth callback (",[71,606,607],{},"\u002Flogin\u002Fcallback",") is now handled entirely server-side — no Vue page needed, no client-side JavaScript",[114,610,611],{},"Token validation happens before the app renders",[114,613,614,615,618],{},"Valid tokens are passed to the client via ",[71,616,617],{},"event.context"," for store hydration",[170,620,621,622,625],{"id":24},"2. ",[71,623,624],{},"authStore.ts"," — client-side source of truth",[49,627,628],{},"The store hydrates itself from the SSR context on first mount, then manages token state client-side:",[76,630,632],{"className":182,"code":631,"language":184,"meta":84,"style":84},"\u002F\u002F stores\u002FauthStore.ts\nexport const useAuthStore = defineStore('auth', () => {\n  const accessToken = ref\u003Cstring | null>(null)\n  const user = ref\u003CUser | null>(null)\n\n  async function hydrate() {\n    \u002F\u002F On SSR, pick up token set by session-guard\n    const nuxtApp = useNuxtApp()\n    const ctx = nuxtApp.ssrContext?.event.context.auth\n\n    if (ctx?.accessToken) {\n      accessToken.value = ctx.accessToken\n      await fetchUser()\n    }\n  }\n\n  async function refreshToken() {\n    const newToken = await $fetch('\u002Fapi\u002Fauth\u002Frefresh', { method: 'POST' })\n    accessToken.value = newToken\n  }\n\n  return { accessToken, user, hydrate, refreshToken }\n})\n",[71,633,634,639,666,697,723,727,741,746,761,773,777,784,794,804,809,813,817,828,856,866,870,874,882],{"__ignoreMap":84},[188,635,636],{"class":190,"line":191},[188,637,638],{"class":194},"\u002F\u002F stores\u002FauthStore.ts\n",[188,640,641,643,646,649,651,654,656,659,662,664],{"class":190,"line":12},[188,642,201],{"class":200},[188,644,645],{"class":200}," const",[188,647,648],{"class":239}," useAuthStore",[188,650,243],{"class":200},[188,652,653],{"class":207}," defineStore",[188,655,212],{"class":211},[188,657,658],{"class":327},"'auth'",[188,660,661],{"class":211},", () ",[188,663,228],{"class":200},[188,665,231],{"class":211},[188,667,668,670,672,674,677,680,683,686,689,692,695],{"class":190,"line":22},[188,669,236],{"class":200},[188,671,431],{"class":239},[188,673,243],{"class":200},[188,675,676],{"class":207}," ref",[188,678,679],{"class":211},"\u003C",[188,681,682],{"class":239},"string",[188,684,685],{"class":200}," |",[188,687,688],{"class":239}," null",[188,690,691],{"class":211},">(",[188,693,694],{"class":239},"null",[188,696,406],{"class":211},[188,698,699,701,704,706,708,710,713,715,717,719,721],{"class":190,"line":252},[188,700,236],{"class":200},[188,702,703],{"class":239}," user",[188,705,243],{"class":200},[188,707,676],{"class":207},[188,709,679],{"class":211},[188,711,712],{"class":207},"User",[188,714,685],{"class":200},[188,716,688],{"class":239},[188,718,691],{"class":211},[188,720,694],{"class":239},[188,722,406],{"class":211},[188,724,725],{"class":190,"line":285},[188,726,289],{"emptyLinePlaceholder":288},[188,728,729,732,735,738],{"class":190,"line":292},[188,730,731],{"class":200},"  async",[188,733,734],{"class":200}," function",[188,736,737],{"class":207}," hydrate",[188,739,740],{"class":211},"() {\n",[188,742,743],{"class":190,"line":304},[188,744,745],{"class":194},"    \u002F\u002F On SSR, pick up token set by session-guard\n",[188,747,748,750,753,755,758],{"class":190,"line":309},[188,749,337],{"class":200},[188,751,752],{"class":239}," nuxtApp",[188,754,243],{"class":200},[188,756,757],{"class":207}," useNuxtApp",[188,759,760],{"class":211},"()\n",[188,762,763,765,768,770],{"class":190,"line":315},[188,764,337],{"class":200},[188,766,767],{"class":239}," ctx",[188,769,243],{"class":200},[188,771,772],{"class":211}," nuxtApp.ssrContext?.event.context.auth\n",[188,774,775],{"class":190,"line":334},[188,776,289],{"emptyLinePlaceholder":288},[188,778,779,781],{"class":190,"line":363},[188,780,544],{"class":200},[188,782,783],{"class":211}," (ctx?.accessToken) {\n",[188,785,786,789,791],{"class":190,"line":382},[188,787,788],{"class":211},"      accessToken.value ",[188,790,354],{"class":200},[188,792,793],{"class":211}," ctx.accessToken\n",[188,795,796,799,802],{"class":190,"line":391},[188,797,798],{"class":200},"      await",[188,800,801],{"class":207}," fetchUser",[188,803,760],{"class":211},[188,805,806],{"class":190,"line":409},[188,807,808],{"class":211},"    }\n",[188,810,811],{"class":190,"line":415},[188,812,412],{"class":211},[188,814,815],{"class":190,"line":420},[188,816,289],{"emptyLinePlaceholder":288},[188,818,819,821,823,826],{"class":190,"line":426},[188,820,731],{"class":200},[188,822,734],{"class":200},[188,824,825],{"class":207}," refreshToken",[188,827,740],{"class":211},[188,829,830,832,835,837,839,842,844,847,850,853],{"class":190,"line":446},[188,831,337],{"class":200},[188,833,834],{"class":239}," newToken",[188,836,243],{"class":200},[188,838,373],{"class":200},[188,840,841],{"class":207}," $fetch",[188,843,212],{"class":211},[188,845,846],{"class":327},"'\u002Fapi\u002Fauth\u002Frefresh'",[188,848,849],{"class":211},", { method: ",[188,851,852],{"class":327},"'POST'",[188,854,855],{"class":211}," })\n",[188,857,858,861,863],{"class":190,"line":451},[188,859,860],{"class":211},"    accessToken.value ",[188,862,354],{"class":200},[188,864,865],{"class":211}," newToken\n",[188,867,868],{"class":190,"line":464},[188,869,412],{"class":211},[188,871,872],{"class":190,"line":479},[188,873,289],{"emptyLinePlaceholder":288},[188,875,876,879],{"class":190,"line":484},[188,877,878],{"class":200},"  return",[188,880,881],{"class":211}," { accessToken, user, hydrate, refreshToken }\n",[188,883,884],{"class":190,"line":489},[188,885,595],{"class":211},[170,887,888,889,892],{"id":27},"3. ",[71,890,891],{},"useAuth.ts"," — component API",[49,894,895],{},"A thin facade over the store, so components never touch Pinia directly:",[76,897,899],{"className":182,"code":898,"language":184,"meta":84,"style":84},"\u002F\u002F composables\u002FuseAuth.ts\nexport function useAuth() {\n  const store = useAuthStore()\n\n  return {\n    isAuthenticated: computed(() => !!store.accessToken),\n    user: computed(() => store.user),\n\n    signIn() {\n      navigateTo(buildOAuthUrl(), { external: true })\n    },\n\n    async signOut() {\n      store.accessToken = null\n      store.user = null\n      deleteCookie('access_token')\n      await navigateTo('\u002Flogin')\n    }\n  }\n}\n",[71,900,901,906,917,930,934,940,959,973,977,984,1001,1006,1010,1020,1030,1039,1050,1064,1068,1072],{"__ignoreMap":84},[188,902,903],{"class":190,"line":191},[188,904,905],{"class":194},"\u002F\u002F composables\u002FuseAuth.ts\n",[188,907,908,910,912,915],{"class":190,"line":12},[188,909,201],{"class":200},[188,911,734],{"class":200},[188,913,914],{"class":207}," useAuth",[188,916,740],{"class":211},[188,918,919,921,924,926,928],{"class":190,"line":22},[188,920,236],{"class":200},[188,922,923],{"class":239}," store",[188,925,243],{"class":200},[188,927,648],{"class":207},[188,929,760],{"class":211},[188,931,932],{"class":190,"line":252},[188,933,289],{"emptyLinePlaceholder":288},[188,935,936,938],{"class":190,"line":285},[188,937,878],{"class":200},[188,939,231],{"class":211},[188,941,942,945,948,951,953,956],{"class":190,"line":292},[188,943,944],{"class":211},"    isAuthenticated: ",[188,946,947],{"class":207},"computed",[188,949,950],{"class":211},"(() ",[188,952,228],{"class":200},[188,954,955],{"class":200}," !!",[188,957,958],{"class":211},"store.accessToken),\n",[188,960,961,964,966,968,970],{"class":190,"line":304},[188,962,963],{"class":211},"    user: ",[188,965,947],{"class":207},[188,967,950],{"class":211},[188,969,228],{"class":200},[188,971,972],{"class":211}," store.user),\n",[188,974,975],{"class":190,"line":309},[188,976,289],{"emptyLinePlaceholder":288},[188,978,979,982],{"class":190,"line":315},[188,980,981],{"class":207},"    signIn",[188,983,740],{"class":211},[188,985,986,989,991,993,996,999],{"class":190,"line":334},[188,987,988],{"class":207},"      navigateTo",[188,990,212],{"class":211},[188,992,473],{"class":207},[188,994,995],{"class":211},"(), { external: ",[188,997,998],{"class":239},"true",[188,1000,855],{"class":211},[188,1002,1003],{"class":190,"line":363},[188,1004,1005],{"class":211},"    },\n",[188,1007,1008],{"class":190,"line":382},[188,1009,289],{"emptyLinePlaceholder":288},[188,1011,1012,1015,1018],{"class":190,"line":391},[188,1013,1014],{"class":200},"    async",[188,1016,1017],{"class":207}," signOut",[188,1019,740],{"class":211},[188,1021,1022,1025,1027],{"class":190,"line":409},[188,1023,1024],{"class":211},"      store.accessToken ",[188,1026,354],{"class":200},[188,1028,1029],{"class":239}," null\n",[188,1031,1032,1035,1037],{"class":190,"line":415},[188,1033,1034],{"class":211},"      store.user ",[188,1036,354],{"class":200},[188,1038,1029],{"class":239},[188,1040,1041,1044,1046,1048],{"class":190,"line":420},[188,1042,1043],{"class":207},"      deleteCookie",[188,1045,212],{"class":211},[188,1047,441],{"class":327},[188,1049,406],{"class":211},[188,1051,1052,1054,1057,1059,1062],{"class":190,"line":426},[188,1053,798],{"class":200},[188,1055,1056],{"class":207}," navigateTo",[188,1058,212],{"class":211},[188,1060,1061],{"class":327},"'\u002Flogin'",[188,1063,406],{"class":211},[188,1065,1066],{"class":190,"line":446},[188,1067,808],{"class":211},[188,1069,1070],{"class":190,"line":451},[188,1071,412],{"class":211},[188,1073,1074],{"class":190,"line":464},[188,1075,1076],{"class":211},"}\n",[62,1078,31],{"id":30},[49,1080,1081],{},"The redirect chain after the rework:",[49,1083,1084,1087],{},[96,1085,1086],{},"Authenticated user"," (the common case):",[76,1089,1092],{"className":1090,"code":1091,"language":81},[79],"\u002Fadmin\u002Fwelcome → \u002Fadmin\u002Fwelcome ✓\n",[71,1093,1091],{"__ignoreMap":84},[49,1095,1096],{},"Zero redirects. The session-guard validates the cookie server-side and lets the request through. The page renders immediately.",[49,1098,1099],{},[96,1100,1101],{},"Fresh login:",[76,1103,1106],{"className":1104,"code":1105,"language":81},[79],"\u002Fadmin\u002Fwelcome → cognitor.test\u002Foauth\u002Fauthorize → \u002Fadmin\u002Fwelcome\n",[71,1107,1105],{"__ignoreMap":84},[49,1109,1110],{},"Two redirects — the minimum possible for OAuth 2.0. The callback page is gone; the session-guard handles it directly.",[1112,1113,1114,1130],"table",{},[1115,1116,1117],"thead",{},[1118,1119,1120,1124,1127],"tr",{},[1121,1122,1123],"th",{},"Scenario",[1121,1125,1126],{},"Before",[1121,1128,1129],{},"After",[1131,1132,1133,1145,1156],"tbody",{},[1118,1134,1135,1139,1142],{},[1136,1137,1138],"td",{},"Authenticated page load",[1136,1140,1141],{},"4 redirects, ~2.2s",[1136,1143,1144],{},"0 redirects",[1118,1146,1147,1150,1153],{},[1136,1148,1149],{},"Fresh login",[1136,1151,1152],{},"4 redirects",[1136,1154,1155],{},"2 redirects (OAuth minimum)",[1118,1157,1158,1161,1164],{},[1136,1159,1160],{},"Login callback",[1136,1162,1163],{},"Vue page renders",[1136,1165,1166],{},"Server redirect, no Vue",[62,1168,34],{"id":33},[49,1170,1171,1172,1175,1176,1179],{},"The ",[71,1173,1174],{},"useRoute"," middleware didn't disappear — it still runs, but now for ",[96,1177,1178],{},"business logic"," (permission checks, feature flags) rather than auth. Security is enforced server-side; the middleware is just convenience.",[49,1181,1171,1182,1184,1185,1188],{},[71,1183,607],{}," and ",[71,1186,1187],{},"\u002Flogin"," routes became server-only. No Vue components, no hydration overhead for pages whose only job is to redirect.",[62,1190,37],{"id":36},[49,1192,1193],{},"Nuxt gives you multiple execution points. Nitro server middleware is the fastest one — it runs before anything Vue-related touches the request. For anything that needs to happen on every request (auth, feature flags, locale detection), that's where it belongs.",[49,1195,1196],{},"Client-side middleware runs after the app boots. That's useful for progressive enhancement and navigation guards within an already-authenticated session — not for the initial auth check that determines whether the user should see the page at all.",[1198,1199,1200],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":84,"searchDepth":12,"depth":12,"links":1202},[1203,1204,1205,1210,1211,1212],{"id":10,"depth":12,"text":11},{"id":14,"depth":12,"text":15},{"id":17,"depth":12,"text":18,"children":1206},[1207,1208,1209],{"id":20,"depth":22,"text":21},{"id":24,"depth":22,"text":25},{"id":27,"depth":22,"text":28},{"id":30,"depth":12,"text":31},{"id":33,"depth":12,"text":34},{"id":36,"depth":12,"text":37},"How moving OAuth handling from client middleware to Nitro server middleware eliminated redundant redirects and made every authenticated page load instant.","md",{},"\u002Fblog\u002Fnuxt-server-side-auth",{"title":40,"description":1213},[1219,1220,1221],"NUXT","AUTH","PERFORMANCE","blog\u002Fnuxt-server-side-auth","n_2i1kT3SbuCAmy5f4bsutp0WB2tm_97HCYfS52YeKA",1781202187665]