Next.js (App Router) app: @blu/next-adapter
The same codebase keeps building and running with next build / next start, and also builds as one federated Blu module with build:blu (Vite). The app's own pages, layouts, components and server actions run inside Blu. The adapter supplies the Blu versions of next/link, next/navigation and next/image, and sends server actions over HTTP with the Blu token.
A complete worked example lives in the Blu repository: teams/team-d/README.md and teams/team-d/BLU_INTEGRATION.patch (a stand-in for HorizonMedia's Unified app). Read it if the app is shaped similarly.
Files to add
| File | What |
|---|---|
blu.module.json |
the manifest, inline: "remote": { "name": "<federation name>", "exposed": "./App" }, remoteEntry: https://<app>/blu/mf-manifest.json |
vite.blu.config.ts |
export default bluModule({ name: "<federation name>", tailwind }) from @blu/next-adapter/vite (pass the app's Tailwind config if it uses Tailwind). Output goes to public/blu/, served by the app's own Next server. |
blu/App.tsx |
what app/layout.tsx does, inside Blu: the app's providers, top bar and guards around {page} (see below) |
blu/server.ts (or similar) |
createBluNextServer({ moduleId, issuer, jwksUrl, apiUrl, roleMap, allowedOrigins }) from @blu/next-adapter/server, configured per environment from env vars |
app/api/blu/actions/route.ts |
where the app's server actions arrive from inside Blu |
app/api/mcp/route.ts |
optional: Concierge tools (createMcpWebHandler from @blu/sdk/server, see server.md) |
package.json: add @blu/sdk, @blu/next-adapter (install.md), vite as a dev dependency, and "build:blu": "vite build -c vite.blu.config.ts". Add public/blu/ to .gitignore. Run build:blu in the deployment build after next build. A running next start only serves files that existed when it started, so restart it after build:blu.
blu/App.tsx: the root layout inside Blu
/// <reference types="vite/client" />
import { useHost } from "@blu/sdk";
import { BluNextModule } from "@blu/next-adapter/client";
import css from "../app/globals.css?inline";
import { Providers } from "../app/providers";
import { AppTopBar } from "../components/AppTopBar";
export default function AppInBlu() {
const blu = useHost(); // user, client, roles from Blu
return (
<BluNextModule
css={css}
pages={import.meta.glob("../app/**/page.tsx", { eager: true })}
layouts={import.meta.glob("../app/*/**/layout.tsx", { eager: true })}
exclude={["/login", "/admin"]} // pages that stay standalone-only
>
{(page) => <Providers user={sessionFromBlu(blu)}><AppTopBar />{page}</Providers>}
</BluNextModule>
);
}
{ eager: true }bundles every page into the module. Navigation is then instant, and the providers and top bar stay mounted, as in Next.js.- Server layouts (
async, or calling server-only code) can't run in the browser. Pass an explicitlayoutsmap, keyed like the glob ("../app/reports/layout.tsx"), to the client component they render (usually a…Shell). Exclude server-only files from the glob with!patterns. Keep the gate the layout enforced: a client-side guard in the UI, plus a check in every server action. - Server pages: a page that's just
redirect("/x")works as is. Anasyncpage that awaits server data needs a small client equivalent, mapped by key inpages, the same way as layouts. - Where data comes from: user and client come from Blu (
useHost()), mapped to the app's own session shape by code the team owns. Data the root layout loaded on the server needs an API route that returns it for the Blu user.
Server side
auth()/ getCurrentUser: first askblu.principalFromHeaders(headers). If it returns a caller, that's the user, mapped to the app's own user and roles. Otherwise, fall through to the existing session lookup, unchanged. Every server action and API route that already callsauth()then works for Blu users, with the same checks.- Server actions inside Blu:
app/api/blu/actions/route.ts:
Keys are action file names underimport * as campaigns from "@/app/actions/campaigns"; export const POST = (req: Request) => blu.actionsRoute({ campaigns }).POST(req);app/actions/(without the extension), and only the modules you list are callable. List only what the Blu pages use, never admin or account actions. The route requires a Blu token, so a cookie alone can't call it. - One client per Blu session: wherever the app checks a user's access to a client, also require that a Blu caller only uses the client Blu chose (
principal.clientId, mapped to the app's id), admins included. - Admin features stay out of Blu: make admin-only gates refuse Blu callers, and don't expose admin API routes to Blu tokens.
- CORS (
proxy.ts/ middleware):bluPreflight(req, allowedOrigins)for preflights, andwithBluCors(req, res, allowedOrigins)on/blu/*(the module files) and on the API routes Blu calls.allowedOrigins= this environment's Blu shell origin(s).
Code that changes
Keep these few and small:
| Where | Change |
|---|---|
| Header components | hide the app's own client switcher, user menu and theme toggle inside Blu: if (useOptionalHost()) return null (Blu's header has them) |
fetch("/api/…") in client code |
moduleFetch("/api/…") from @blu/next-adapter/client (plain fetch standalone; the app's own server, with the token, inside Blu) |
Images or files the app serves only to signed-in users (<img src="/api/…">) |
useModuleAssetUrl(src) (and preloadModuleAsset for preloads) |
<Image src="/logo.svg"> |
nothing: the adapter loads root-relative images from the app's own server |
next/font, next-themes |
not used inside Blu; Blu's page owns fonts and theme. Don't render ThemeProvider in blu/App.tsx. |
Search the client code for fetch(, "/api/, src="/, window.location and document.documentElement to find what needs attention.