Back to Blog
Monolithic-First Authentication Beyond Next.js: Making It Work in a Split Client–Server Architecture
authenticationnextjsarchitecturebackendfrontend

Monolithic-First Authentication Beyond Next.js: Making It Work in a Split Client–Server Architecture

A deep dive into the challenges of using monolithic-first authentication systems in decoupled client–server architectures, and why a proxy-based approach can bridge the gap.

If you’ve ever tried using a Next.js-first auth library in a decoupled frontend–backend setup and felt like you were fighting the tooling, this post is for you.

Modern web applications increasingly adopt decoupled architectures i.e. a separate frontend, a separate backend, independent deployments, and clean API boundaries. This model scales well, enforces ownership, and avoids framework lock-in.

However, not all tooling is designed with this separation in mind.

In this post, I want to talk about monolithic-first authentication systems, auth solutions that assume a shared client–server runtime and the friction they introduce when used in a split client–server architecture. I’ll also walk through why I ended up using a proxy-based approach to make things work without sacrificing developer experience.


The Setup: A Cleanly Decoupled Architecture

The architecture was simple and intentional:

  • Client: a standalone frontend application (Vite or Nextjs)

  • Server: a separate backend API (Node.js + Express)

  • Independent deployments

  • Clear HTTP boundary between frontend and backend

This setup is common when:

  • Teams want flexibility

  • Backend needs to serve multiple clients

  • Infrastructure must scale independently

From an architectural perspective, authentication should fit neatly into this boundary. That’s where the friction began.

What “Monolithic-First Authentication” Means

By monolithic-first authentication, I mean auth systems that:

  • Are optimized for frameworks where client and server share the same runtime

  • Assume same-origin requests

  • Rely heavily on framework-level features (cookies, middleware, server components)

Tools like Better-Auth fall into this category. They are designed primarily for frameworks like Next.js, where frontend and backend logic coexist in the same application.

This isn’t a flaw, it’s a design choice.

And inside a monolithic Next.js app, it works extremely well.

Why Monolithic-First Auth Breaks Down in a Split Architecture

Monolithic-first authentication systems work exceptionally well inside frameworks where client and server share the same runtime. Features like a unified auth client, automatic cookie handling, and middleware-level session access depend on that assumption.

In a decoupled client–server setup, this shared context disappears, forcing auth state to cross an explicit HTTP boundary. While backend frameworks may still be supported, the developer experience degrades, auth logic fragments, session handling diverges, and the abstractions that made the system valuable begin to leak.


The Core Issue: authClient Assumes a Shared Runtime

The heart of the problem is simple:

Monolithic-first auth systems assume that the client and server live in the same framework boundary.

authClient assumes:

  • Same-origin cookies

  • Framework-level request context

  • Middleware-level session access

Once you split the app:

  • Those assumptions break

  • Auth stops being “plug and play”

  • You either give up DX or start bending the architecture


The Conventional Approach: Server-Only Auth Integration

The most straightforward way to use monolithic-first authentication in a split client–server setup is to push all authentication logic entirely to the backend.

In this model:

  • The auth service (e.g. Better Auth) is integrated only on the server
  • Auth routes are exposed as backend APIs (login, logout, session, refresh, etc.)
  • The frontend communicates with these APIs over HTTP
  • The frontend treats authentication as just another remote service

Architecturally, this approach is sound.

It preserves the clean client–server boundary, avoids cross-runtime coupling, and works reliably in production. Many teams adopt this pattern by default.

Conventional method

Why This Still Falls Short

While this approach works functionally, it comes with a major trade-off:

You lose the full value of the auth client provided by the service.

Most monolithic-first auth systems ship with a rich client abstraction (authClient) designed to:

  • Automatically manage sessions
  • Handle cookie-based auth seamlessly
  • Expose typed helpers like useSession, signIn, signOut
  • Integrate deeply with framework features

When auth is pushed entirely to the backend:

  • The frontend can no longer use authClient directly
  • You end up re-wrapping auth flows with custom fetch calls
  • Session state must be manually synced and cached
  • Auth logic becomes fragmented between client and server

At that point, the frontend is no longer consuming an auth SDK — it’s consuming a set of bespoke APIs.

The Core Trade-off

So the choice becomes:

  • Architectural purity, but reduced developer experience
  • Great DX, but only inside a monolithic framework boundary

Neither option is ideal when you want both:

  • A cleanly decoupled architecture
  • And the productivity benefits of a first-class auth client

This tension is what ultimately led me to look for a different approach.


The Proxy-Based Solution: Recreating the Shared Runtime Boundary

The breakthrough came from reframing the problem.

Instead of forcing the auth system to adapt to a split architecture, I asked:

What if the client could still believe it was talking to a monolithic backend?

The idea is simple:

  • Keep the auth service integrated where it works best (the backend)
  • Introduce a thin proxy layer that mirrors the auth service’s expected routes
  • Forward requests from the client to the backend without breaking assumptions

This proxy does not implement authentication logic itself. It merely preserves the contract that monolithic-first auth systems expect.

// src/app/api/auth/[...all]/route.ts

import { NextRequest, NextResponse } from "next/server";

const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL!;

async function proxy(req: NextRequest) {
  const url = new URL(req.url);
  const target = BACKEND_URL + url.pathname + url.search;

  const res = await fetch(target, {
    method: req.method,
    headers: {
      cookie: req.headers.get("cookie") ?? "",
      authorization: req.headers.get("authorization") ?? "",
      "content-type": req.headers.get("content-type") ?? "",
    },
    body: req.body,
    redirect: "manual",
  });

  return new NextResponse(res.body, {
    status: res.status,
    headers: res.headers,
  });
}

export const GET = proxy;
export const POST = proxy;

⚠️ Security Check

This API route intentionally behaves as a transparent auth proxy. All request headers (including cookies and authorization) are forwarded as-is because the backend authentication service is already responsible for enforcing security guarantees such as CSRF protection, origin validation, session verification, and secure cookie configuration.

The proxy itself is not a security boundary and does not perform trust checks.

Do not replicate this pattern unless your backend authentication layer is correctly hardened. Blindly forwarding headers without proper backend security controls can introduce serious vulnerabilities.

What the Proxy Actually Does

At a high level, the proxy:

  • Exposes the same auth endpoints expected by authClient
  • Forwards requests to the real backend auth service
  • Preserves headers, cookies, and request context
  • Returns responses untouched

From the client’s perspective:

  • authClient works as-is
  • Sessions behave like a monolithic app
  • No custom wrappers or fetch logic are required

From the backend’s perspective:

  • Auth remains fully server-controlled
  • No client secrets or logic leak to the frontend
  • The existing auth integration stays intact

Why This Works

Monolithic-first auth systems don’t actually require a monolith.

They require:

  • Stable routes
  • Predictable request/response shapes
  • Consistent cookie behavior
  • A shared mental model of “where auth lives”

The proxy recreates that illusion without collapsing the architecture.

You’re not merging client and server. You’re simulating the boundary they expect.


The Resulting Architecture

With the proxy in place:

  • The frontend stays framework-agnostic
  • The backend remains independently deployable
  • authClient retains its full DX benefits
  • No auth logic is duplicated or rewritten

You get the best of both worlds:

  • Decoupled infrastructure
  • Monolithic-grade developer experience

Proxy method

An Important Constraint

This approach works best when:

  • The auth system is HTTP-based
  • Client-side auth relies on predictable routes
  • Cookie or header-based sessions are used

If the auth system relies on deep framework internals or compile-time hooks, a proxy won’t help.

But for most modern auth providers, this pattern is surprisingly effective.

Trade-offs to Be Aware Of

This approach isn’t free:

  • You introduce one extra network hop
  • Proxy routes must stay in sync with backend auth routes
  • Debugging auth may involve two layers instead of one

For most teams, this is a reasonable trade-off for preserving DX and architectural clarity.


When This Pattern Makes Sense

This proxy-based approach is not a default recommendation, it is a deliberate architectural compromise.

It makes sense when:

  • You want a fully decoupled frontend–backend architecture
  • Your auth provider offers a rich client SDK optimized for monolithic frameworks
  • Reimplementing auth flows would significantly hurt DX
  • You are willing to own a thin proxy layer as part of your infrastructure

It is likely not worth it when:

  • Your frontend and backend already live in the same framework
  • You are using token-only auth with no client SDK
  • Your team prefers explicit APIs over SDK abstractions
  • The auth system relies on deep framework internals

This pattern exists to resolve a very specific tension, not to replace simpler setups.


Closing Thoughts

Monolithic-first authentication systems are not “bad”, they are simply optimized for a different set of assumptions.

Problems arise when those assumptions go unexamined and are forced onto architectures they were never designed for.

The proxy-based approach works not because it is clever, but because it respects both sides of the equation:

  • The auth system’s need for stable, predictable boundaries
  • The application’s need for decoupling, scalability, and ownership

Rather than bending the architecture to fit the tool or abandoning the tool to preserve purity, the proxy reframes the boundary itself.

Sometimes the cleanest solution isn’t choosing between monoliths and microservices.

It’s understanding what the tooling actually needs, and giving it just enough illusion to work without lying to your architecture.

Related Posts

Designing a Real-Time Voice AI Architecture with WebSockets and LLMs

A deep dive into the system architecture behind real-time voice AI, exploring how WebSockets, streaming STT, LLMs, and TTS work together to deliver low-latency conversational experiences.

Topics

voice-aiarchitecturewebsockets+2 more
READ MORE
Redis in Distributed System Design: Beyond Caching to Rate Limiting, Locks & Queues

A production-focused deep dive into how Redis fits into distributed system design, covering real-world patterns like rate limiting, caching, distributed locks, background jobs, and the trade-offs engineers must understand.

Topics

redissystem-designdistributed-systems+4 more
READ MORE
Web Application Firewall (WAF): How It Protects Modern Web Applications

An in-depth introduction to Web Application Firewalls, where they sit in the stack, common web attacks they prevent, and how to deploy WAFs effectively.

Topics

securitywafweb-security+2 more
READ MORE