React & Next.js · 8 min read

Next.js Multi-Zones: Micro-Frontends in an Enterprise Monorepo

Path-based micro-frontends with Next.js: when the split pays off, how to wire it in a monorepo, and the cross-cutting problems the docs skip.

By Praful Patel

Short answer

Multi-zones serve several independently deployed Next.js apps under one domain, each owning a set of URL paths. It is the right tool when teams need separate release cycles or you are migrating a site piece by piece. It is the wrong tool for sharing components. Every move between zones is a full page load, and every zone ships its own framework runtime, so draw zone boundaries where users rarely cross them.

“Micro-frontends” covers everything from iframes to runtime module loading. Next.js supports one specific form natively: vertical splitting by path, which the docs call multi-zones. This guide covers when that split is worth what it costs, how to configure it in a monorepo with Next.js 16, and the cross-cutting problems (auth, Server Actions, shared UI, version skew) that the official guide only touches on.

Do you actually need zones?

Most of the reasons teams give for micro-frontends have cheaper answers inside a single app:

Problem Try first Zones justified when
Slow builds Turbopack, build caching, fewer statically generated pages at build One area pulls in heavy dependencies the rest of the site never uses
Team ownership Monorepo packages, CODEOWNERS, route groups Teams need to deploy independently, not just own folders
Different layouts per area Route groups with separate root layouts Never, on its own
Legacy migration Rewrite in place You want to move a site to Next.js one path at a time, with the old app still serving the rest
Different frameworks None Another zone really is a different stack

If the honest answer is “we want clearer code ownership”, a well-structured monorepo with one deployable app gives you that without the costs below.

How a zone works

A zone is an ordinary Next.js app with two properties. It serves its own paths (the blog zone has its routes under app/blog/), and it sets assetPrefix so its /_next/ JavaScript and CSS don’t collide with another zone’s. The default zone, which serves every path no other zone claims, needs no prefix.

// apps/blog/next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  assetPrefix: '/blog-static',        // assets served from /blog-static/_next/...
  transpilePackages: ['@acme/ui'],    // internal package shipped as TypeScript source
  experimental: {
    serverActions: {
      // Users reach this app through www.example.com, not its own host.
      allowedOrigins: ['www.example.com'],
    },
  },
};

export default nextConfig;

Two details are easy to miss. assetPrefix only covers /_next/ assets, so files in the zone’s public/ folder are not prefixed, and two zones that both ship /logo.svg will collide (see the assetPrefix reference). And the extra beforeFiles rewrite for static assets that older tutorials include is no longer needed from Next.js 15.

Routing requests to the right zone

Something has to send /blog/* to the blog app. The docs recommend rewrites in the default app, because they add the least latency:

// apps/web/next.config.ts (the default zone)
import type { NextConfig } from 'next';

const BLOG = process.env.BLOG_ZONE_URL;           // e.g. https://blog.internal.example.com
const DASHBOARD = process.env.DASHBOARD_ZONE_URL;

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      { source: '/blog', destination: `${BLOG}/blog` },
      { source: '/blog/:path+', destination: `${BLOG}/blog/:path+` },
      { source: '/blog-static/:path+', destination: `${BLOG}/blog-static/:path+` },
      { source: '/dashboard', destination: `${DASHBOARD}/dashboard` },
      { source: '/dashboard/:path+', destination: `${DASHBOARD}/dashboard/:path+` },
      { source: '/dashboard-static/:path+', destination: `${DASHBOARD}/dashboard-static/:path+` },
    ];
  },
};

export default nextConfig;

Use proxy.ts (formerly middleware) only when routing needs a runtime decision, such as a feature flag that sends a path to the new zone during a migration. A load balancer or CDN can do the same job in front of all the apps. On Vercel, the Microfrontends feature handles that routing at the platform level from a microfrontends.json file. Each path must belong to exactly one zone. Two zones serving /blog is a routing conflict, not a fallback.

Navigation: the cost you cannot configure away

Within a zone, navigation is a normal soft navigation. Between zones it is a hard navigation: the browser unloads the page, downloads the other app’s runtime and boots it. Next.js <Link> tries to prefetch and soft-navigate, which cannot work across apps, so cross-zone links must be plain <a> tags.

Encode that in a shared component, so nobody has to remember which paths belong to which app:

// packages/config/zones.ts
export const ZONES = { blog: ['/blog'], dashboard: ['/dashboard'] } as const;
export type Zone = keyof typeof ZONES | 'web';

export function zoneFor(path: string): Zone {
  for (const [zone, prefixes] of Object.entries(ZONES)) {
    if (prefixes.some((p) => path === p || path.startsWith(`${p}/`))) return zone as Zone;
  }
  return 'web';
}

// packages/ui/zone-link.tsx
import Link from 'next/link';
import type { ComponentProps } from 'react';
import { zoneFor, type Zone } from '@acme/config/zones';

type Props = Omit<ComponentProps<'a'>, 'href'> & { href: string; currentZone: Zone };

export function ZoneLink({ href, currentZone, ...rest }: Props) {
  return zoneFor(href) === currentZone
    ? <Link href={href} {...rest} />   // same app: soft navigation and prefetch
    : <a href={href} {...rest} />;     // other app: full page load
}

The design rule follows from this: pages users move between often should live in the same zone. Splitting product listing and product detail into separate zones turns the most common journey on the site into a series of full reloads.

Monorepo layout

apps/
  web/          # default zone: marketing, everything unclaimed
  blog/         # /blog/*, assetPrefix /blog-static
  dashboard/    # /dashboard/*, assetPrefix /dashboard-static
packages/
  ui/           # design system, ZoneLink, shared header/footer
  config/       # zones map, ESLint, tsconfig bases
  auth/         # shared token verification
microfrontends.json

For local development, Turborepo has a built-in microfrontends proxy. It reads microfrontends.json and serves all running apps from one port (3024 by default). Each Next.js app starts with next dev --port $(turbo get-mfe-port). That gets you closer to production routing than running three apps on three ports and following links across them by hand. The same file is a subset of Vercel’s schema, so it carries over if you deploy there.

Cross-cutting concerns

Concern What happens with zones What to do
Sessions Users only ever see one origin, so a host-only session cookie is sent on every path, whichever zone serves it Each zone verifies the session itself. Use a shared package, never a header added by another zone
Server Actions The user-facing host differs from the zone’s own host Set serverActions.allowedOrigins in every zone that uses actions
Shared header/footer Each zone bundles its own copy at its own version Keep the shell small and ship changes to it in all zones, or put new behaviour behind flags
Analytics and consent Each zone boots its own scripts on a hard navigation One shared package, one consent store (a cookie, not memory)
404s Unclaimed paths fall through to the default zone The default zone owns the global not-found page
Caching and ISR Each zone has its own cache and revalidation endpoints A CMS webhook must reach every zone that renders the content

The session row matters most. A zone that trusts identity passed in from another zone becomes as weak as the weakest route into it. Auth patterns that hold up in this setup are in Next.js proxy and JWT authentication. Per-zone caching and self-hosting trade-offs are in Deploying Next.js.

Version skew between zones

Independent deploys mean zones run different versions of shared packages at the same time. Users will notice the header change between / and /blog. Pick a policy up front: either changes to the shared shell are released to all zones together (a pipeline that triggers every app when packages/ui changes), or the shell is versioned and behaviour changes are gated by feature flags, which the Next.js docs suggest for releases that span zones.

Multi-zones, Module Federation or platform microfrontends?

Multi-zones Module Federation Vercel Microfrontends
Unit of composition URL paths Components loaded at runtime URL paths
Next.js support Built in, documented for the App Router Third-party plugin. Its maintainers have announced it is winding down, and it doesn’t cover the App Router Platform feature built on the zone model
Routing lives in Rewrites, proxy, or your load balancer Host application Vercel’s network, from microfrontends.json
Hosting Anywhere Anywhere Vercel

For a Next.js estate in 2026, path-based zones are the supported option. If the real requirement is several teams contributing widgets to one page, that is a shared package and a release process, not runtime composition.

Checklist

  • Every zone except the default sets a unique assetPrefix, and public/ file names don’t collide.
  • Each path prefix belongs to exactly one zone, defined once in a shared map.
  • Cross-zone links use <a> through a shared component. Journeys users take often stay inside one zone.
  • Every zone verifies sessions itself and sets serverActions.allowedOrigins.
  • CMS revalidation webhooks fan out to every zone that renders the content.
  • Local development runs through the microfrontends proxy, not separate ports.

Questions people actually ask

Can zones share client-side state?
Not in memory. A hard navigation discards the JavaScript heap. Anything that has to survive a move between zones belongs in the URL, a cookie, browser storage or the server.

Do multi-zones affect SEO?
Not directly: crawlers see one domain and ordinary server-rendered pages. Watch the operational details instead: one coherent sitemap (or a sitemap index pointing at each zone’s sitemap), consistent canonical and trailing-slash rules across apps, and a single 404 owner.

Splitting a large Next.js platform across teams?

I help engineering leads decide whether multi-zones are worth their cost, draw zone boundaries around real user journeys, and set up the monorepo, routing and release process so teams can deploy independently.

Plan your multi-zone architecture →
← Back to all insights