@econneq/apollo-next-multitenancy
v1.0.5
Published
Industrial-grade Apollo abstraction for Next.js App Router with multi-tenancy support. Auto-detects server/client context, injects x-tenant-id, and routes through BFF or direct backend.
Maintainers
Readme
@econneq/apollo-next-multitenancy
Industrial-grade Apollo abstraction for Next.js App Router with first-class multi-tenancy.
What it solves
| Pain Point | How this package fixes it |
|---|---|
| Server vs Client Apollo setup | One factory per context — no boilerplate |
| Forgetting x-tenant-id | Injected automatically on every request |
| BFF wiring | createBffHandler is a one-liner proxy |
| Middleware tenant extraction | createTenantMiddleware with 4 strategies |
| Single vs multi-tenant | Config flag — same codebase for both |
Architecture
Browser (Client Component)
│ useQuery / useMutation ← x-tenant-id auto-injected
▼
/api/graphql (BFF)
│ createBffHandler ← validates auth, enriches headers
▼
http://backend:8000/graphql ← x-tenant-id forwarded
Server Component / Server Action
│ query() / mutation() ← x-tenant-id from next/headers
▼
http://backend:8000/graphql ← direct call, no BFF hopInstallation
npm install @econneq/apollo-next-multitenancy \
@apollo/client \
@apollo/experimental-nextjs-app-support \
graphqlor
pnpm add @econneq/apollo-next-multitenancy@latest @apollo/client @apollo/experimental-nextjs-app-support graphql next-auth --filter school-front-coreQuick Start (5 files)
1. Middleware — extract tenant on every request
Option A — Simple (no existing middleware):
Use createTenantMiddleware when your app doesn't already have a custom middleware:
// middleware.ts
import { createTenantMiddleware } from "@econneq/apollo-next-multitenancy/middleware";
export const middleware = createTenantMiddleware({
strategy: "subdomain", // acme.app.com → "acme"
fallback: "default", // for localhost
});
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};Option B — Custom middleware (already have i18n / URL rewriting):
If you already have a middleware that handles subdomain extraction, locale detection,
or URL rewriting — skip createTenantMiddleware entirely. Just stamp x-tenant-id
directly inside your existing middleware:
// middleware.ts — inside your existing proxy/router middleware
const domain = extractSubdomain(request.headers.get('host') || '')
const response = NextResponse.rewrite(rewrittenUrl)
// ── Add these lines — that's all the package needs ──
const tenantId = domain !== 'public'
? domain
: process.env.TENANT_ID ?? null
if (tenantId) {
// Read by BFF (createBffHandler)
response.headers.set('x-tenant-id', tenantId)
// Read by Server Components via next/headers()
response.headers.set('x-middleware-request-x-tenant-id', tenantId)
}
// ────────────────────────────────────────────────────See
examples/with-custom-middleware/middleware.tsfor a complete real-world example with subdomain extraction, locale detection, and URL rewriting combined.
2. Server client — one-time setup
// lib/apollo-server.ts
import { createServerApolloClient } from "@econneq/apollo-next-multitenancy/server";
export const { query, mutation } = createServerApolloClient({
serverUrl: process.env.INTERNAL_GRAPHQL_URL,
// Option B (custom middleware): no tenant config needed —
// x-tenant-id is already in next/headers() from your middleware.
//
// Option A (createTenantMiddleware): add a resolver:
// tenant: { resolveTenantId: subdomainTenantResolver },
});3. BFF route
// app/api/graphql/route.ts
import { createBffHandler } from "@econneq/apollo-next-multitenancy/bff";
const { GET, POST } = createBffHandler({
upstreamUrl: process.env.INTERNAL_GRAPHQL_URL!,
// x-tenant-id is forwarded automatically from incoming request headers
});
export { GET, POST };4. Root layout — wrap with provider
For apps using a [domain] URL segment (from a custom middleware rewrite):
// app/[locale]/[domain]/layout.tsx
import { headers } from "next/headers";
import { ApolloMultitenantProvider } from "@econneq/apollo-next-multitenancy/client";
export default async function DomainLayout({ children, params }) {
const { domain } = await params;
const headerStore = await headers();
// Prefer middleware header (authoritative), fall back to URL segment
const tenantId = headerStore.get("x-tenant-id") ?? domain;
return (
<ApolloMultitenantProvider tenantId={tenantId} clientUrl="/api/graphql">
{children}
</ApolloMultitenantProvider>
);
}For apps using a flat layout (with createTenantMiddleware):
// app/layout.tsx
import { headers } from "next/headers";
import { ApolloMultitenantProvider } from "@econneq/apollo-next-multitenancy/client";
export default async function RootLayout({ children }) {
const tenantId = (await headers()).get("x-tenant-id");
return (
<html>
<body>
<ApolloMultitenantProvider tenantId={tenantId} clientUrl="/api/graphql">
{children}
</ApolloMultitenantProvider>
</body>
</html>
);
}5. Use in Server and Client Components
// app/dashboard/page.tsx (Server Component — direct backend call)
import { query } from "@/lib/apollo-server";
import { gql } from "@apollo/client";
const GET_DASHBOARD = gql`query GetDashboard { dashboard { tenantName plan } }`;
export default async function Page() {
const { data } = await query(GET_DASHBOARD);
return <h1>{data.dashboard.tenantName}</h1>;
}// components/UserList.tsx (Client Component — routes through BFF)
"use client";
import { useQuery } from "@econneq/apollo-next-multitenancy/client";
import { gql } from "@apollo/client";
const GET_USERS = gql`query GetUsers { users { id name } }`;
export function UserList() {
const { data, loading } = useQuery(GET_USERS); // x-tenant-id injected automatically
if (loading) return <p>Loading...</p>;
return <ul>{data?.users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}Tenant Strategies
| Strategy | Use case | Example |
|---|---|---|
| subdomain | tenant.app.com | SaaS with per-tenant subdomains |
| path | /tenant/page | Path-based multi-tenancy |
| header | Forward existing header | Behind API gateway |
| custom | Any logic | Session, JWT, DB lookup |
Already have a custom middleware? You don't need any of these strategies. Just set
x-tenant-idandx-middleware-request-x-tenant-idheaders directly as shown in Option B above.
Single-tenant setup
createServerApolloClient({ tenant: { tenantId: "my-company" } });
createBffHandler({ tenant: { tenantId: "my-company" }, upstreamUrl: "..." });
<ApolloMultitenantProvider tenantId="my-company" />;
// Or via env var:
TENANT_ID=my-companyBFF: Auth-gated proxy
// app/api/graphql/route.ts
import { getServerSession } from "next-auth";
import { createBffHandler } from "@econneq/apollo-next-multitenancy/bff";
const { POST, GET } = createBffHandler({
upstreamUrl: process.env.INTERNAL_GRAPHQL_URL!,
async onRequest(req) {
const session = await getServerSession();
if (!session) return Response.json({ error: "Unauthorized" }, { status: 401 });
},
async enrichHeaders(req, headers) {
const session = await getServerSession();
return { ...headers, "x-user-id": session?.user?.id ?? "" };
},
});Examples
The examples/ folder contains copy-paste ready setups:
| Folder | When to use |
|---|---|
| examples/with-simple-middleware/ | New app, no existing middleware |
| examples/with-custom-middleware/ | Already have a proxy / i18n / rewrite middleware |
The with-custom-middleware example is based on a real production multi-tenant platform
with subdomain routing + locale detection. Copy the whole folder as a starting point.
API Reference
/server
| Export | Description |
|---|---|
| createServerApolloClient(options) | Returns { getClient, query, mutation } |
/client
| Export | Description |
|---|---|
| ApolloMultitenantProvider | Root layout wrapper |
| useQuery(doc, options?) | Drop-in Apollo useQuery with tenant header |
| useMutation(doc, options?) | Drop-in Apollo useMutation with tenant header |
| useTenantContext() | Access { tenantId, headerName } |
/bff
| Export | Description |
|---|---|
| createBffHandler(options) | Returns { GET, POST } for Next.js route handlers |
Main entry
| Export | Description |
|---|---|
| subdomainTenantResolver | Built-in subdomain extractor |
| envTenantResolver | Built-in env var resolver |
| resolveTenantId | Core resolution logic |
| All types | TenantConfig, ApolloMultitenantOptions, etc. |
Environment Variables
| Variable | Used by | Description |
|---|---|---|
| INTERNAL_GRAPHQL_URL | Server, BFF | Your backend GraphQL URL |
| NEXT_PUBLIC_GRAPHQL_URL | Server (fallback) | Public GraphQL URL |
| TENANT_ID | All | Fixed tenant ID (single-tenant) |
| NEXT_PUBLIC_TENANT_ID | Client (fallback) | Fixed tenant ID (browser-visible) |
| NEXT_PUBLIC_ROOT_DOMAIN | Custom middleware | Root domain for subdomain extraction |
License
MIT
