The Strangler Fig pattern is the de facto standard for migrating legacy monoliths, but it hits a concrete wall immediately: Authentication.
You have a legacy PHP application (Laravel, Symfony, or vanilla) serving the root domain. You deploy a Next.js App Router instance to handle specific routes (e.g., /dashboard/analytics). You configure your load balancer (Nginx/AWS ALB) to route traffic correctly.
The browser sends the PHPSESSID (or laravel_session) cookie to the Next.js server. However, your Next.js application treats the user as unauthenticated.
The Root Cause: Serialization Incompatibility
The issue is not network reachability or cookie scope. If both apps sit on example.com, the browser transmits the cookies to both backends.
The failure occurs at the deserialization layer.
- Storage Format: PHP sessions are typically stored on the file system (
/var/lib/php/sessions) or Redis. PHP uses a proprietary serialization format (e.g.,user_id|i:42;email|s:12:"...") that allows it to store instantiatable classes. - Runtime Isolation: Your Next.js Node/Edge runtime has no access to the PHP server's file system.
- Decoding Logic: Even if Next.js could read the Redis store, Node.js cannot natively deserialize PHP objects. Trying to implement a PHP unserializer in JavaScript is fragile and a security risk (remote code execution via object injection).
The Fix: The "Signed JWT Sidecar"
Do not rewrite your authentication logic in Next.js yet. Do not try to parse PHP session files in Node.
Instead, modify the PHP application to act as the Identity Provider (IdP). When PHP creates or updates a session, it will issue a secondary, lightweight, cryptographically signed cookie (a JWT) specifically for the Next.js consumer.
This approach keeps PHP as the Source of Truth while allowing Next.js to verify identity stateless at the Edge.
Phase 1: The Legacy PHP Bridge
We need a mechanism in PHP that hooks into the login flow to set a bridge_token cookie.
Prerequisites: PHP 8.2+, OpenSSL enabled.
Create a service class to handle the token generation. We use HMAC-SHA256 with a shared secret.
<?php
// src/Auth/NextJsBridge.php
declare(strict_types=1);
namespace App\Auth;
class NextJsBridge
{
// In production, load this from .env
private string $secret = 'fc89b...shared_secret_must_be_32_chars...';
public function attachBridgeCookie(int $userId, string $role): void
{
$payload = [
'sub' => $userId,
'role' => $role,
'iat' => time(),
'exp' => time() + (3600 * 2), // 2 hour expiry matches PHP session
];
$jwt = $this->generateJwt($payload);
// Set a cookie accessible to the entire domain
setcookie(
'auth_bridge',
$jwt,
[
'expires' => $payload['exp'],
'path' => '/',
'domain' => '.example.com', // Critical: enable subdomains/path sharing
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]
);
}
private function generateJwt(array $payload): string
{
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
$payloadJson = json_encode($payload);
$base64UrlHeader = $this->base64UrlEncode($header);
$base64UrlPayload = $this->base64UrlEncode($payloadJson);
$signature = hash_hmac(
'sha256',
$base64UrlHeader . "." . $base64UrlPayload,
$this->secret,
true
);
$base64UrlSignature = $this->base64UrlEncode($signature);
return "{$base64UrlHeader}.{$base64UrlPayload}.{$base64UrlSignature}";
}
private function base64UrlEncode(string $data): string
{
return str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode($data)
);
}
}
Implementation Point: Call attachBridgeCookie immediately after your user successfully logs in within your PHP controller (or Laravel Event Listener).
// src/Controller/LoginController.php
public function login(Request $request, NextJsBridge $bridge)
{
// ... existing login logic ...
// Once validated:
$bridge->attachBridgeCookie($user->id, $user->role);
// ... redirect ...
}
Phase 2: The Next.js Consumer
In Next.js, we do not need a database connection. We simply verify the signature of the auth_bridge cookie using the same secret.
We will use jose because it is lightweight and compatible with the Next.js Edge Runtime (unlike the native crypto module in some contexts).
Installation: npm install jose
1. The Verification Utility
Create a strictly typed session utility.
// lib/auth.ts
import { jwtVerify } from 'jose';
import { cookies } from 'next/headers';
const SECRET_KEY = new TextEncoder().encode(
process.env.BRIDGE_SECRET || 'fc89b...shared_secret_must_be_32_chars...'
);
export interface BridgeSession {
userId: number;
role: string;
isAuthenticated: boolean;
}
export async function getSession(): Promise<BridgeSession> {
const cookieStore = cookies();
const token = cookieStore.get('auth_bridge')?.value;
if (!token) {
return { userId: 0, role: 'guest', isAuthenticated: false };
}
try {
const { payload } = await jwtVerify(token, SECRET_KEY, {
algorithms: ['HS256'],
});
return {
userId: Number(payload.sub),
role: payload.role as string,
isAuthenticated: true,
};
} catch (error) {
// Token expired or invalid signature
return { userId: 0, role: 'guest', isAuthenticated: false };
}
}
2. Middleware Protection (Optional but Recommended)
If you want to protect specific routes at the network edge, add this to middleware.ts.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';
const SECRET_KEY = new TextEncoder().encode(process.env.BRIDGE_SECRET);
export async function middleware(request: NextRequest) {
const token = request.cookies.get('auth_bridge')?.value;
const loginUrl = new URL('/login', request.url); // Redirect to PHP login
if (!token) {
return NextResponse.redirect(loginUrl);
}
try {
await jwtVerify(token, SECRET_KEY);
return NextResponse.next();
} catch (err) {
// If token is invalid/expired, force re-login via PHP
return NextResponse.redirect(loginUrl);
}
}
export const config = {
matcher: ['/dashboard/:path*', '/protected/:path*'],
};
3. Accessing User Data in Server Components
Now you can access user context anywhere in your React Server Components without prop drilling or client-side fetches.
// app/dashboard/page.tsx
import { getSession } from '@/lib/auth';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const session = await getSession();
if (!session.isAuthenticated) {
// Fallback if middleware didn't catch it
redirect('/login');
}
return (
<main className="p-8">
<h1 className="text-2xl font-bold">
Welcome back, User {session.userId}
</h1>
<div className="mt-4 p-4 bg-slate-100 rounded">
<p>Your role is: {session.role}</p>
{/* Render role-based components here */}
</div>
</main>
);
}
Why This Architecture Works
1. Separation of Concerns
PHP remains the "Write" authority for sessions. It handles login attempts, 2FA, password resets, and bans. Next.js becomes a "Read-Only" consumer. You don't have to reimplement complex auth rules in JavaScript immediately.
2. Performance (Zero Latency)
A common alternative is having Next.js call a PHP API endpoint (e.g., GET /api/me) on every request to validate the session. This introduces network latency and doubles the load on your legacy PHP server. By using a signed JWT, verification happens instantly in the Next.js runtime (CPU-bound) without making any upstream network requests (IO-bound).
3. Graceful Logout
When the user logs out in PHP:
- PHP destroys the server-side session.
- PHP deletes the
auth_bridgecookie. - Next.js instantly sees the missing cookie and considers the user unauthenticated.
Conclusion
The Strangler Fig pattern relies on seamless interoperability. By implementing a "Sidecar Token"—a standard JWT generated by your legacy system specifically for your modern stack—you bridge the gap between stateful (PHP) and stateless (Next.js/Edge) architectures. This allows you to migrate one route at a time without disrupting the user experience.