If your Next.js frontend and Laravel backend auth setup feels fragile, it usually is. Most teams are not fighting authentication itself. They are fighting session boundaries, cookie scope, CSRF expectations, and mismatched assumptions between browser, frontend app, and API server.
The fix is not another random middleware tweak. The fix is picking a clean architecture and being consistent about it across local development and production.
For a Next.js Laravel auth stack, my strong opinion is simple: let Laravel own authentication and session state, let the browser carry the cookies, and let Next.js act as the application UI layer, not a second auth server pretending to be smarter than the first one.
Stop mixing session auth and token auth without a reason
A lot of broken setups come from trying to combine Laravel Sanctum session auth, custom JWT flows, and frontend-side auth abstractions in one stack. That usually creates more surface area, not more flexibility.
If your product is a normal browser-based SaaS app, use cookie-based session auth with Laravel and keep it boring.
That means:
- Laravel handles login, logout, session creation, CSRF, authorization, and user identity
-
Next.js calls Laravel with
credentials: 'include' - The browser stores and sends cookies automatically
Protected user state is fetched from Laravel, not reinvented in the frontend
Use token auth only when you genuinely need it, like:mobile clients
third-party API consumers
machine-to-machine access
public API products
For a browser app, cookie sessions are usually the right answer because they align with how browsers already work.
The architecture that avoids most auth bugs
The cleanest setup looks like this:
Production domains
-
Frontend:
app.qcode.in -
Backend:
api.qcode.in -
Session domain:
.qcode.in### Local development domains
Do not build auth on localhost chaos if you can avoid it. Use consistent local domains instead:
-
Frontend:
app.qcode.test -
Backend:
api.qcode.test -
Session domain:
.qcode.testThen make Laravel explicit.
// .env
APP_URL=https://api.qcode.test
FRONTEND_URL=https://app.qcode.test
SESSION_DOMAIN=.qcode.test
SANCTUM_STATEFUL_DOMAINS=app.qcode.test,api.qcode.test,app.qcode.in,api.qcode.in
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=lax
And keep CORS strict enough to work, not loose enough to hide mistakes.
// config/cors.php
return [
'paths' => ['api/*', 'login', 'logout', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => [
'https://app.qcode.test',
'https://app.qcode.in',
],
'allowed_headers' => ['*'],
'supports_credentials' => true,
];
Using * with credentials is not valid. Be explicit.
The request flow that actually works
When using Laravel Sanctum with session auth, the browser flow matters.
Step 1: Prime CSRF
Before login, ask Laravel for the CSRF cookie.
await fetch('https://api.qcode.test/sanctum/csrf-cookie', {
method: 'GET',
credentials: 'include',
});
Step 2: Log in with cookies enabled
Then log in with credentials included and standard AJAX headers.
await fetch('https://api.qcode.test/login', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify({ email, password }),
});
Step 3: Fetch the authenticated user
After login, fetch the user from Laravel. Do not invent a second source of truth.
const response = await fetch('https://api.qcode.test/api/user', {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
});
const user = await response.json();
If you want a reusable client in Next.js App Router, keep it thin.
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL!;
export async function apiFetch(path: string, init: RequestInit = {}) {
return fetch(`${API_BASE}${path}`, {
...init,
credentials: 'include',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
...(init.headers ?? {}),
},
});
}
That is the core loop. No fake local auth cache. No duplicated token parsing. No fragile frontend-side session emulation.
Where most teams break the boundary
This stack usually fails in the same places.
1. Wrong cookie domain
If the session cookie is bound to api.qcode.in instead of .qcode.in, your frontend app on app.qcode.in will not behave the way you expect.
2. Missing credentials: 'include'
If your fetch client does not include credentials, you are not doing session auth. You are doing anonymous requests and hoping for magic.
3. Bad CORS config
Laravel must explicitly allow the frontend origin and credentials.
4. Trying to read HttpOnly cookies in client code
You should not need to. That is the whole point of HttpOnly cookies. Let the browser send them.
5. SSR assumptions that do not match browser reality
If a page is rendered on the server, your Next.js server runtime may not automatically have the same cookie context as the userβs browser session. That means you need a deliberate strategy.
The two sane options are:
- render auth-sensitive screens from the client after loading the user
- forward cookies through route handlers or server components intentionally Do not casually mix both patterns across the app.
What I would ship in a real product
For most internal SaaS or dashboard-style products, this setup is hard to beat:
- Laravel for auth, sessions, policies, and user data
- Sanctum for SPA session auth
- Next.js App Router for UI and product surface
- subdomain-based separation between frontend and backend
- HTTPS in every environment that matters
- explicit CORS and cookie settings
minimal auth state in the frontend
I would avoid:bolting NextAuth on top of Laravel session auth unless there is a very specific need
storing user auth state in multiple places
debugging cookies on
localhostfor weeks instead of using proper local domainsmixing SSR, edge middleware, client auth guards, and token refresh logic without a clear boundary
The big idea is simple: one system should own auth. In this stack, that system should usually be Laravel.
Once you accept that, the implementation gets much less confusing.
If your current Next.js Laravel auth setup feels unstable, stop patching symptoms. Redraw the boundary. Let Laravel own the session, let the browser carry the cookie, and let Next.js focus on shipping product features instead of roleplaying as an identity provider.
Official docs worth keeping open while implementing this:
- Laravel Sanctum: https://laravel.com/docs/sanctum
- Laravel session config: https://laravel.com/docs/session
- Next.js App Router: https://nextjs.org/docs/app
- MDN Fetch credentials: https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
Top comments (0)