Vercel Logo

Feature Flag Routing

Route users to different applications based on a flag. Roll out a new dashboard to 10% of users, or A/B test a redesign.

Outcome

Configure the microfrontends routing to use feature flags for dynamic routing decisions.

The Use Case

You've built a new dashboard in a separate microfrontend. You want to:

  1. Test it with internal users first
  2. Roll out to 10% of users
  3. Gradually increase to 100%
  4. Remove the old dashboard

Without feature flags, you'd have to do a big-bang migration. With flags, you control the rollout.

How It Works

With feature flag routing, flagged paths go to the default application first, which decides where to route:

Request: /app/dashboard

1. Microfrontends sees /app is flagged
2. Routes to default app (marketing) instead of dashboard
3. Default app middleware checks flag
4. If flag enabled: routes to dashboard-v2
5. If flag disabled: routes to dashboard (original)

This is different from regular routing where paths go directly to the matching app.

Fast Track

  1. Add flag property to routing configuration
  2. Add runMicrofrontendsMiddleware to default app
  3. Create the new dashboard-v2 app
  4. Test both variants

Hands-on Exercise 4.1

Add feature flag routing for a new dashboard version.

Part 1: Create Dashboard V2 App

Create a simplified new dashboard:

cd apps
pnpm create next-app@latest dashboard-v2 --typescript --tailwind --eslint --app --src-dir=false --import-alias="@/*"

Update apps/dashboard-v2/package.json:

apps/dashboard-v2/package.json
{
  "name": "@acme/dashboard-v2",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "dev": "next dev --port $(microfrontends port)",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "16.1.1",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "@acme/ui": "workspace:*",
    "@vercel/microfrontends": "latest"
  }
}

Create apps/dashboard-v2/next.config.ts:

apps/dashboard-v2/next.config.ts
import type { NextConfig } from "next";
import { withMicrofrontends } from "@vercel/microfrontends/next/config";
 
const nextConfig: NextConfig = {};
 
export default withMicrofrontends(nextConfig);

Create apps/dashboard-v2/app/app/page.tsx (note the nested structure):

apps/dashboard-v2/app/app/page.tsx
import { Header } from "@acme/ui";
 
export default function NewDashboardPage() {
  return (
    <div>
      <Header />
      <main className="p-8">
        <div className="rounded-lg bg-gradient-to-r from-blue-500 to-purple-600 p-8 text-white">
          <h1 className="text-4xl font-bold">Dashboard V2</h1>
          <p className="mt-4 text-lg">
            Welcome to the redesigned dashboard experience!
          </p>
        </div>
      </main>
    </div>
  );
}

Part 2: Update Microfrontends Configuration

Update apps/marketing/microfrontends.json with the feature flag:

apps/marketing/microfrontends.json
{
  "$schema": "https://openapi.vercel.sh/microfrontends.json",
  "applications": {
    "@acme/marketing": {
      "development": {
        "fallback": "http://localhost:3000",
        "local": 3000
      }
    },
    "@acme/docs": {
      "routing": [
        { "paths": ["/docs", "/docs/:path*"] }
      ],
      "development": {
        "local": 3001
      }
    },
    "@acme/dashboard": {
      "routing": [
        { "paths": ["/settings", "/settings/:path*"] },
        {
          "paths": ["/app", "/app/:path*"],
          "flag": "new-dashboard"
        }
      ],
      "development": {
        "local": 3002
      }
    },
    "@acme/dashboard-v2": {
      "routing": [
        {
          "paths": ["/app", "/app/:path*"],
          "flag": "new-dashboard"
        }
      ],
      "development": {
        "local": 3003
      }
    }
  },
  "options": {
    "localProxyPort": 3024
  }
}

Both dashboard apps claim /app/*, but the flag property determines which one gets the request based on the flag evaluation.

Part 3: Add Microfrontends Middleware

The default app (marketing) needs middleware to handle flagged routing decisions.

Create apps/marketing/middleware.ts:

apps/marketing/middleware.ts
import type { NextRequest } from "next/server";
import { runMicrofrontendsMiddleware } from "@vercel/microfrontends/next/middleware";
 
export async function middleware(request: NextRequest) {
  const response = await runMicrofrontendsMiddleware({
    request,
    flagValues: {
      "new-dashboard": async () => {
        // Simple cookie-based flag for demo
        const cookie = request.cookies.get("new-dashboard");
        return cookie?.value === "true";
      },
    },
  });
 
  if (response) {
    return response;
  }
}
 
export const config = {
  matcher: [
    // Required for prefetch optimizations
    "/.well-known/vercel/microfrontends/client-config",
    // Flagged paths
    "/app",
    "/app/:path*",
  ],
};

Important: The middleware matcher must include:

  • /.well-known/vercel/microfrontends/client-config - For prefetch optimizations
  • All flagged paths - So middleware runs for these requests

Part 4: Deploy and Test

Feature flag routing doesn't work with the local proxy. You'll get "Duplicate path" errors. Deploy to Vercel to test it.

Deploy to Vercel:

git add -A
git commit -m "feat: add feature flag routing for dashboard v2"
git push

Test without flag (routes to original dashboard):

  1. Visit your Vercel preview URL at /app
  2. You should see the original dashboard

Test with flag (routes to dashboard-v2):

  1. Open DevTools console
  2. Set the cookie: document.cookie = "new-dashboard=true; path=/"
  3. Refresh the /app page
  4. You should see Dashboard V2 with the gradient hero

Local development workaround:

For local development, you can only run one version at a time. Comment out either @acme/dashboard or @acme/dashboard-v2 in microfrontends.json to test each app individually

Production Feature Flags

In production, integrate with a feature flag service:

apps/marketing/middleware.ts
import type { NextRequest } from "next/server";
import { runMicrofrontendsMiddleware } from "@vercel/microfrontends/next/middleware";
import { getFlag } from "@vercel/flags/next";
 
export async function middleware(request: NextRequest) {
  const response = await runMicrofrontendsMiddleware({
    request,
    flagValues: {
      "new-dashboard": async () => {
        // Use Vercel Flags or any flag service
        return await getFlag("new-dashboard", request);
      },
    },
  });
 
  if (response) {
    return response;
  }
}

If using the Flags SDK, share FLAGS_SECRET across all microfrontends in the group.

Testing with Validation

Use the testing utility to verify flagged routing:

apps/marketing/__tests__/feature-flags.test.ts
/* @jest-environment node */
import { validateMiddlewareOnFlaggedPaths } from "@vercel/microfrontends/next/testing";
import { middleware } from "../middleware";
 
describe("feature flag routing", () => {
  test("routes correctly for flagged paths", async () => {
    await expect(
      validateMiddlewareOnFlaggedPaths("./microfrontends.json", middleware)
    ).resolves.not.toThrow();
  });
});

Commit

git add -A
git commit -m "feat: add feature flag routing for dashboard v2"

Done-When

  • Dashboard V2 app created and configured
  • flag property added to routing in microfrontends.json
  • runMicrofrontendsMiddleware added to default app
  • Middleware matcher includes flagged paths
  • Understand that feature flag routing requires Vercel deployment
  • (After deploy) Can toggle between dashboards via cookie

Rollout Strategy

PhaseFlag ValueTraffic
Internal testingSpecific users~1%
Beta10% rollout10%
Gradual50% rollout50%
Full100% rollout100%
CleanupRemove flag, update routing100% to v2

What's Next

What if you're migrating from a legacy system that isn't on Vercel? Next: the strangler fig pattern for incremental migration.