This guide walks you through building a micro-frontend architecture using Next.js 15 and Webpack Module Federation 2.0. You will have a working multi-app setup with shared components and independent deployments within roughly three to four hours, depending on your existing project structure.

What You'll Build

  • A host Next.js 15 application that dynamically loads remote micro-frontends at runtime
  • A standalone remote app that exposes shared UI components over the network
  • A shared authentication and session context that works across both apps without duplicating code
  • An independent deployment pipeline where each app builds and ships without blocking the other

Prerequisites

Before you start, make sure you have the following in place.

  • Node.js 20 or higher and pnpm 9 installed locally
  • Familiarity with Next.js App Router and basic React patterns
  • Two separate Git repositories, one for the host app and one for the remote app
  • A Vercel account or any Node-compatible hosting environment for deployment

Why Micro-Frontends Matter in 2026

By mid-2026, most product teams at Australian and Singaporean SaaS companies running more than three frontend developers are hitting a familiar problem. A single Next.js monolith becomes a coordination bottleneck. Teams wait on each other to deploy. A single failing test blocks the whole pipeline.

Micro-frontends solve this by splitting a frontend into independently owned vertical slices. Each team owns a route, a feature domain, or a UI section. They build, test, and deploy on their own schedule. Module Federation 2.0, shipped with Webpack 5.90+ and now stable as of early 2026, makes this achievable without a custom build toolchain.

This is not a theoretical pattern. Companies like Zalando, Shopify, and SEEK have used variations of this in production for years. The tooling has matured enough that smaller product teams can now adopt it without a platform engineering team.

Step 1: Scaffold the Host Application

What does the host app do?

The host app is the shell. It owns the top-level routing, the navigation, and the page layout. It fetches and renders components from remote apps at runtime using Module Federation.

Create a new Next.js 15 project using the official CLI.

pnpm create next-app@latest host-app --typescript --app --tailwind
cd host-app
pnpm add @module-federation/nextjs-mf

The @module-federation/nextjs-mf package, maintained by the Module Federation core team, bridges Webpack Module Federation 2.0 with Next.js App Router. As of version 8.x (released Q1 2026), it supports React Server Components with some constraints covered in Step 3.

What if the package install fails?

If you see a peer dependency conflict, add --legacy-peer-deps to the pnpm command. This is a known issue when mixing Next.js 15 canary builds with the stable module federation package.

Step 2: Configure Module Federation in the Host

Open next.config.ts and add the Module Federation plugin. Replace the placeholder URL with your remote app's deployed origin.

import { NextFederationPlugin } from '@module-federation/nextjs-mf';
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  webpack(config, options) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'host',
        remotes: {
          remote_app: `remote_app@${process.env.REMOTE_APP_URL}/_next/static/chunks/remoteEntry.js`,
        },
        shared: {
          react: { singleton: true, requiredVersion: '^19.0.0' },
          'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
        },
        filename: 'static/chunks/remoteEntry.js',
      })
    );
    return config;
  },
};

export default nextConfig;

Set the REMOTE_APP_URL environment variable in your .env.local file during development. Point it to http://localhost:3001 for now.

The singleton: true flag on React is critical. Without it, both apps load separate React instances and you get hook errors that are difficult to debug.

Step 3: Scaffold and Expose the Remote Application

In a separate directory, scaffold the remote app. This app runs on port 3001 locally and exposes specific components to the host.

pnpm create next-app@latest remote-app --typescript --app --tailwind
cd remote-app
pnpm add @module-federation/nextjs-mf

Configure its next.config.ts to expose a component.

import { NextFederationPlugin } from '@module-federation/nextjs-mf';
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  webpack(config, options) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'remote_app',
        exposes: {
          './DashboardWidget': './src/components/DashboardWidget.tsx',
        },
        shared: {
          react: { singleton: true, requiredVersion: '^19.0.0' },
          'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
        },
        filename: 'static/chunks/remoteEntry.js',
      })
    );
    return config;
  },
};

export default nextConfig;

Add a script to run on port 3001 in package.json.

{
  "scripts": {
    "dev": "next dev -p 3001"
  }
}

Why must exposed components be Client Components?

As of Module Federation 2.0 and Next.js 15, exposed remote components must be marked with 'use client' at the top of the file. The federation runtime loads them via dynamic import, which only works in the client bundle. Attempting to expose a Server Component will cause a silent hydration failure at runtime.

'use client';

export default function DashboardWidget() {
  return (
    <div className="rounded-xl border p-4">
      <h2 className="text-lg font-semibold">Revenue Widget</h2>
      <p className="text-sm text-gray-500">Loaded from remote_app</p>
    </div>
  );
}

Step 4: Consume the Remote Component in the Host

Back in the host app, create a dynamic import for the remote component. Wrap it in React Suspense to handle the async load gracefully.

'use client';

import dynamic from 'next/dynamic';
import { Suspense } from 'react';

const DashboardWidget = dynamic(
  () => import('remote_app/DashboardWidget'),
  { ssr: false }
);

export default function DashboardPage() {
  return (
    <main className="p-8">
      <h1 className="text-2xl font-bold mb-4">Dashboard</h1>
      <Suspense fallback={<div>Loading widget...</div>}>
        <DashboardWidget />
      </Suspense>
    </main>
  );
}

Set ssr: false. The remote entry script is not available during server-side rendering unless you configure a server-side federation setup, which adds significant complexity and is only warranted for SEO-critical pages.

Start both apps in separate terminals and open http://localhost:3000. You should see the DashboardWidget from the remote app rendering inside the host page.

Step 5: Share Authentication State Across Apps

How do you share session context without duplicating logic?

The cleanest approach is to own authentication entirely in the host and pass a lightweight session token to remote components via props or a shared context module. Do not replicate auth logic in the remote app.

Create a shared session hook in the host.

'use client';

import { createContext, useContext } from 'react';

interface Session {
  userId: string;
  role: 'admin' | 'viewer';
}

export const SessionContext = createContext<Session | null>(null);

export function useSession() {
  const ctx = useContext(SessionContext);
  if (!ctx) throw new Error('useSession must be used inside SessionProvider');
  return ctx;
}

Expose this hook from the host via Module Federation by adding it to the host's exposes block. The remote app then imports useSession from host/SessionContext instead of managing its own auth state.

This pattern keeps a single source of truth. Teams building remote apps never need to touch token handling or refresh logic.

Step 6: Set Up Independent Deployment Pipelines

Each app deploys independently. The key constraint is deployment order: deploy the remote app first, then the host. The host reads the remote entry URL at runtime, so the remote must be live before users hit the host.

A minimal GitHub Actions workflow for the remote app looks like this.

name: Deploy Remote App
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - run: pnpm install --frozen-lockfile
      - run: pnpm build
      - uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_REMOTE_PROJECT_ID }}
          vercel-args: '--prod'

Create a matching workflow for the host app with its own VERCEL_HOST_PROJECT_ID. Use a repository dispatch or a workflow dependency if you want the host to redeploy automatically after the remote deploys successfully.

What if the remote app is unavailable at runtime?

Wrap every federated import in an error boundary. If the remote entry fails to load, the host renders a fallback UI rather than a full-page crash. This is the most important production resilience pattern for micro-frontends, and it is the one most tutorials skip.

'use client';

import { Component, ReactNode } from 'react';

export class FederationErrorBoundary extends Component<
  { children: ReactNode; fallback: ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  render() {
    if (this.state.hasError) return this.props.fallback;
    return this.props.children;
  }
}

Common Pitfalls to Avoid

  • Mismatched React versions: Both apps must declare the same React version range in their shared config. A mismatch causes hook errors that are hard to trace.
  • Hardcoding remote URLs: Always use environment variables for remote entry URLs. Hardcoded URLs break staging and preview environments.
  • Over-sharing modules: Only share libraries that truly need to be singletons (React, React DOM, session context). Sharing too many packages increases bundle coordination complexity without meaningful benefit.
  • Skipping error boundaries: A remote app outage should never take down the host. Wrap every dynamic federation import in an error boundary.

Frequently Asked Questions

Does this work with the Next.js App Router?

Yes, with constraints. Exposed remote components must be Client Components marked with 'use client'. Server Components cannot be federated across app boundaries as of Module Federation 2.0 and Next.js 15 in late 2026.

Can I use Turbopack instead of Webpack for this setup?

Not yet. Module Federation is a Webpack-specific feature. Turbopack does not support Module Federation as of October 2026. The @module-federation/nextjs-mf package forces the Webpack compiler even when Turbopack is enabled in your Next.js config.

How is this different from Next.js multi-zones?

Multi-zones use separate Next.js apps stitched together via URL rewrites at the CDN or proxy level. Module Federation shares JavaScript modules at runtime without a full page reload. Multi-zones are simpler but cause full navigations between apps. Federation enables seamless client-side transitions.

What happens to performance with multiple remote entries?

Each remote entry is an additional network request on first load. Bundle sizes stay manageable because shared singletons (React, shared context) are deduplicated. In practice, teams report a 5 to 15 percent increase in initial load time for the first federated component, which Suspense fallbacks mitigate visually.

Do I need a separate deployment for each micro-frontend?

Yes, and that is the point. Each remote app deploys to its own Vercel project, Cloudflare Pages site, or Node server. Independent deployments are what enable independent team cadences. If you share a deployment, you lose most of the organisational benefit.

Next Steps

Once your two-app setup is running, the natural next step is adding a shared design system. The host can expose a component library and all remote apps consume it, so UI updates propagate across the entire product from one deployment. Pair this with a Storybook instance to document the shared components across teams.

You should also evaluate your observability setup. When errors happen in a federated component, the stack trace can point to the remote app's bundle. Tools like Sentry with source map uploads for each app independently keep debugging manageable.

If your team is exploring how to structure frontend architecture for a product that is scaling, the engineers at Lenka Studio work with SaaS and product teams across Australia, Singapore, Canada, and the US on exactly these patterns. Whether you are migrating a monolith or starting fresh, getting the architecture right early saves months of rework later. Reach out to discuss your setup and we can help you map the right approach for your team size and deployment constraints.