Recommended resources
The freely available resources this lesson will direct you to.
Welcome to the final and most integrative module of our course, "End-to-End System Integration." Over the past nine modules, we have designed agent hierarchies, built tool and prompt stores, implemented state management, and established robust observability. Now, it's time to assemble these components into a single, cohesive, production-ready service.
This first lesson focuses on creating the "front door" to our entire system: the main API endpoint. Our goal is to build an endpoint that accepts user requests and, critically, extracts a tenant identifier from each one. This identifier is the key that will unlock all the tenant-specific configurations, agents, and tools we've so carefully designed.
The Concept of Scoped Context
Before we write any code, let's visualize our goal. In a multi-tenant system, every incoming request must be processed within a specific context. This context includes the tenant making the request, their authentication status, and a trace ID for observability. Our task today is to establish the 'Tenant' part of this context at the earliest possible moment—right when the request enters our system.

Tenant Identification Strategies
To associate a request with a tenant, the client must provide a tenant identifier. There are several standard architectural patterns for this. As a developer building a backend service, choosing the right strategy is a key design decision.
Vercel's documentation provides a concise overview of the three most common tenant identification strategies. This will give you a solid foundation for the patterns we'll discuss.
Please read the section Tenant identification strategies. Pay attention to the code snippets for subdomain-based, custom domain-based, and path-based identification.
The three strategies are:
- Subdomain-based (
tenant1.yourapp.com): Excellent for user-facing SaaS applications where each customer gets a personalized URL. It's clean and scales well. - Path-based (
/api/tenant1/chat): A RESTful and explicit approach. The tenant is part of the resource path, which is easy to log and route. - Header-based (
X-Tenant-ID: ...): Very common for internal services and APIs. The client explicitly passes the tenant identifier in a custom HTTP header. This decouples tenant identification from the URL structure, offering flexibility.
For our goal of building a backend service that might be called by various internal clients, the header-based approach is a robust and flexible choice. However, it's valuable to understand the trade-offs.
Implementing Multi-Tenancy in Next-Auth (Auth.js) + Next.js
This video, "Implementing Multi-Tenancy in Next-Auth," offers a great discussion on the pros and cons of different approaches, particularly contrasting a session-based method with a URL path-based one.
Watch the segment from the beginning. The speaker's reasoning for preferring a URL-based approach—to allow a user to interact with multiple tenants in different browser tabs—highlights the importance of ensuring each request is self-contained and explicitly states its context. This principle applies equally well to header-based identification in an API context.
The Middleware Pattern for Centralized Identification
Regardless of the strategy chosen, the logic for identifying the tenant should be centralized. We don't want to repeat this logic in every single API route. In Next.js, the perfect place for this is middleware.
A middleware file (middleware.ts) in your project root will intercept incoming requests before they reach your API routes or pages. This allows us to:
- Inspect the request (its path, headers, etc.).
- Extract the tenant identifier.
- Validate it (e.g., check if it exists).
- Attach the identifier to the request so downstream handlers can use it.
- Or, if the identifier is missing or invalid, reject the request immediately.
This pattern keeps our API route handlers clean and focused on their core business logic.
Let's revisit the Vercel documentation to see this middleware pattern in action.
Read the sections on How middleware resolves tenants and Tenant context. Notice how the middleware extracts the tenant and then uses response.headers.set to pass information along. We will use a similar but slightly more modern approach to modify the request headers.
Implementation: Creating the Entry Point
Let's now build our API endpoint and the accompanying middleware. We'll implement a header-based strategy using a custom header, x-tenant-id.
Step 1: Create the Middleware
In the root of your Next.js project (at the same level as your app directory), create a file named middleware.ts. This file will contain the logic to inspect incoming requests for our tenant header.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// We only want to run this middleware for our API routes
if (request.nextUrl.pathname.startsWith('/api')) {
const tenantId = request.headers.get('x-tenant-id');
// If the tenant ID header is missing, reject the request
if (!tenantId) {
return new NextResponse(
JSON.stringify({ success: false, message: 'Missing x-tenant-id header' }),
{ status: 400, headers: { 'content-type': 'application/json' } }
);
}
// The tenant ID is present. We can now add it to the request headers
// so our API route can access it.
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-resolved-tenant-id', tenantId); // Use a different name to indicate it's been processed
// Forward the request to the API route with the new header
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
// For all other routes, do nothing
return NextResponse.next();
}
// Configure the middleware to run on specific paths
export const config = {
matcher: '/api/:path*',
};
In this code:
- We check for the
x-tenant-idheader on any request to/api/*. - If it's missing, we immediately return a
400 Bad Requesterror. - If it's present, we create a new set of headers, add the resolved ID, and forward the modified request to its destination using
NextResponse.next(). This is a clean, secure way to pass context from middleware to an API route.
Step 2: Create the API Route Handler
Now, let's create the API route that will receive the processed request. Create a new file at app/api/chat/route.ts.
Inside this handler, we need to read the header that our middleware added.
Next.js 14 Tutorial - 41 - Headers in Route Handlers
The "Next.js 14 Tutorial" by Codevolution provides a clear, practical demonstration of how to read headers within a Route Handler.
Please watch the section covering the two methods for reading request headers. We will be using the second method, which leverages the headers() function from next/headers.
With that knowledge, your API route handler will look very simple:
// app/api/chat/route.ts
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const headerList = headers();
const tenantId = headerList.get('x-resolved-tenant-id');
// In a real application, you would now use this tenantId to fetch
// tenant-specific data from your database.
console.log(`Received request for tenant: ${tenantId}`);
// Placeholder logic: just acknowledge the request and echo the tenantId
return NextResponse.json({
success: true,
message: `Request processed for tenant ${tenantId}`,
});
}
This route handler is now free of any identification logic. It simply trusts that if the request has reached it, the x-resolved-tenant-id header is present and reliable, thanks to our middleware.
To test this, you can use a tool like Postman, Insomnia, or a simple curl command:
Successful Request:
curl -X POST http://localhost:3000/api/chat \
-H "Content-Type: application/json" \
-H "x-tenant-id: tenant-abc-123" \
-d '{}'
// Expected Response:
// {"success":true,"message":"Request processed for tenant tenant-abc-123"}
Failed Request (missing header):
curl -X POST http://localhost:3000/api/chat \
-H "Content-Type: application/json" \
-d '{}'
// Expected Response:
// {"success":false,"message":"Missing x-tenant-id header"}
Conclusion
In this lesson, you've successfully built the entry point for our entire agentic system. You've established a robust and secure pattern for identifying tenants that separates concerns and keeps your API logic clean.
Key Takeaways:
- Tenant identification is the first step: Before any agentic logic runs, you must know which tenant the request belongs to.
- Centralize logic with middleware: Next.js middleware is the ideal location to implement tenant identification, providing a single point of control and validation for all API requests.
- Pass context securely via headers: Modifying request headers in middleware is the standard pattern for passing resolved information, like a
tenantId, to your API route handlers. - API routes focus on business logic: By offloading identification to middleware, your route handlers can focus on their primary task, confident that the necessary context is available.
You've now laid the foundation for the entire end-to-end flow. In our next lesson, we will take the tenantId we've successfully extracted and use it to perform the next critical step: resolving the full tenant context by loading tenant-specific agents, tools, and prompts from the stores we built in previous modules.