DEV Community

Cover image for Next.js + Laravel Auth: A Clear Path to Manage Session Boundaries
Saqueib Ansari
Saqueib Ansari

Posted on • Originally published at qcode.in

Next.js + Laravel Auth: A Clear Path to Manage Session Boundaries

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.test Then 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
Enter fullscreen mode Exit fullscreen mode

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,
];
Enter fullscreen mode Exit fullscreen mode

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',
});
Enter fullscreen mode Exit fullscreen mode

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 }),
});
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 ?? {}),
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

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 localhost for weeks instead of using proper local domains

  • mixing 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:

Top comments (0)