Including multi-tenant config into tRPC context

Hey all, I've been working on upgrading my app to support multi-tenancy, inspired by Vercel's Platforms starter kit. The core of the relevant logic comes down to this Next.js middleware:
export const config = {
matcher: [
/*
* Match all paths except for:
* 1. /api routes
* 2. /_next (Next.js internals)
* 3. /_static (inside /public)
* 4. all root files inside /public (e.g. /favicon.ico)
* 5. All files inside subdirectories of /public/static
*/
'/((?!api/|_next/|_static/|static/|_vercel|[\\w-]+\\.\\w+).*)',
],
};

export default function middleware(req: NextRequest) {
const url = req.nextUrl;

// Get hostname of request (e.g. demo.vercel.pub, demo.localhost:3000)
const hostname = req.headers
.get('host')!
.replace('.localhost:3000', `.${ROOT_DOMAIN}`);

const searchParams = req.nextUrl.searchParams.toString();
const path = `${url.pathname}${
searchParams.length > 0 ? `?${searchParams}` : ''
}`;

// Rewrite root to `/app`
if (hostname === 'localhost:3000' || hostname === ROOT_DOMAIN) {
return NextResponse.rewrite(
new URL(`/app${path === '/' ? '' : path}`, req.url),
);
}

const tenantSlug = hostname.replace(`.${ROOT_DOMAIN}`, '');

// Rewrite everything else to `/[tenantSlug]`
return NextResponse.rewrite(new URL(`/${tenantSlug}${path}`, req.url));
}
export const config = {
matcher: [
/*
* Match all paths except for:
* 1. /api routes
* 2. /_next (Next.js internals)
* 3. /_static (inside /public)
* 4. all root files inside /public (e.g. /favicon.ico)
* 5. All files inside subdirectories of /public/static
*/
'/((?!api/|_next/|_static/|static/|_vercel|[\\w-]+\\.\\w+).*)',
],
};

export default function middleware(req: NextRequest) {
const url = req.nextUrl;

// Get hostname of request (e.g. demo.vercel.pub, demo.localhost:3000)
const hostname = req.headers
.get('host')!
.replace('.localhost:3000', `.${ROOT_DOMAIN}`);

const searchParams = req.nextUrl.searchParams.toString();
const path = `${url.pathname}${
searchParams.length > 0 ? `?${searchParams}` : ''
}`;

// Rewrite root to `/app`
if (hostname === 'localhost:3000' || hostname === ROOT_DOMAIN) {
return NextResponse.rewrite(
new URL(`/app${path === '/' ? '' : path}`, req.url),
);
}

const tenantSlug = hostname.replace(`.${ROOT_DOMAIN}`, '');

// Rewrite everything else to `/[tenantSlug]`
return NextResponse.rewrite(new URL(`/${tenantSlug}${path}`, req.url));
}
This middleware is set up to explicitly exclude requests to /api Now, within my tRPC router, it is helpful in certain places to be able to access the tenantSlug value. I could go and change all my tRPC queries/mutations to take that as input and change client-side code everywhere but that would be hundreds of functions and even more calls in the client code. I have been thinking about how to get this be part of tRPC's context, but I'm a bit stuck Currently my context looks like this (i have a mix of HTTP and WebSockets)
export async function createContext(
ctx: CreateWSSContextFnOptions | CreateNextContextOptions,
): Promise<{
session: Session | null;
req: IncomingMessage | NextApiRequest;
res: ws | NextApiResponse;
prisma: ExtendedPrismaClient;
}> {
const { req, res } = ctx;

let session: Session | null;
try {
// @ts-expect-error -- This will fail if called client side
session = await getServerSession(req, res, getAuthOptions(req));
} catch (error) {
session = await getSession(ctx);
}

const contextInner = await createContextInner();

return {
...contextInner,
session,
req,
res,
};
}
export async function createContext(
ctx: CreateWSSContextFnOptions | CreateNextContextOptions,
): Promise<{
session: Session | null;
req: IncomingMessage | NextApiRequest;
res: ws | NextApiResponse;
prisma: ExtendedPrismaClient;
}> {
const { req, res } = ctx;

let session: Session | null;
try {
// @ts-expect-error -- This will fail if called client side
session = await getServerSession(req, res, getAuthOptions(req));
} catch (error) {
session = await getSession(ctx);
}

const contextInner = await createContextInner();

return {
...contextInner,
session,
req,
res,
};
}
Since the middleware ignores requests to /api - the req object in context (in case of HTTP calls) doesn't include the subdomain/slug in the rquest headers. I figured someone here had to have run into this in the past and may have figured out a solutin already - so any help is appreciated!
Solution:
For anyone who stumbles upon this later, my fix for now is to do this: ```ts export async function createContext( ctx: CreateWSSContextFnOptions | CreateNextContextOptions,...
Jump to solution
3 Replies
Solution
haardik | LearnWeb3
For anyone who stumbles upon this later, my fix for now is to do this:
export async function createContext(
ctx: CreateWSSContextFnOptions | CreateNextContextOptions,
): Promise<{
session: Session | null;
req: IncomingMessage | NextApiRequest;
res: ws | NextApiResponse;
prisma: ExtendedPrismaClient;
tenant: string | null;
}> {
const { req, res } = ctx;

const rewriteHeader = req.headers['x-nextjs-rewrite'] as string | undefined;
const tenant = rewriteHeader?.split('tenant=')[1] ?? null;

let session: Session | null;
try {
// @ts-expect-error -- This will fail if called client side
session = await getServerSession(req, res, getAuthOptions(req));
} catch (error) {
session = await getSession(ctx);
}

const contextInner = await createContextInner();

return {
...contextInner,
session,
req,
res,
tenant: tenant ?? contextInner.tenant,
};
}

/** Use this helper for:
* - trpc's `createSSGHelpers` where we don't have req/res
* */
export async function createContextInner(opts?: CreateInnerContextOptions) {
if (!opts) {
return {
session: null,
prisma,
tenant: null,
};
}

if (opts.ctx) {
const rewriteHeader = opts.ctx.req.headers['x-nextjs-rewrite'] as
| string
| undefined;
const tenant = rewriteHeader?.split('tenant=')[1] ?? null;

let session: Session | null;
try {
session = await getServerSession(
opts.ctx.req,
opts.ctx.res,
// @ts-expect-error -- This will fail if called client side
getAuthOptions(opts.ctx.req),
);
} catch (error) {
session = await getSession(opts.ctx);
}

return {
session,
prisma,
tenant,
};
}

return {
session: null,
prisma,
tenant: null,
};
}
export async function createContext(
ctx: CreateWSSContextFnOptions | CreateNextContextOptions,
): Promise<{
session: Session | null;
req: IncomingMessage | NextApiRequest;
res: ws | NextApiResponse;
prisma: ExtendedPrismaClient;
tenant: string | null;
}> {
const { req, res } = ctx;

const rewriteHeader = req.headers['x-nextjs-rewrite'] as string | undefined;
const tenant = rewriteHeader?.split('tenant=')[1] ?? null;

let session: Session | null;
try {
// @ts-expect-error -- This will fail if called client side
session = await getServerSession(req, res, getAuthOptions(req));
} catch (error) {
session = await getSession(ctx);
}

const contextInner = await createContextInner();

return {
...contextInner,
session,
req,
res,
tenant: tenant ?? contextInner.tenant,
};
}

/** Use this helper for:
* - trpc's `createSSGHelpers` where we don't have req/res
* */
export async function createContextInner(opts?: CreateInnerContextOptions) {
if (!opts) {
return {
session: null,
prisma,
tenant: null,
};
}

if (opts.ctx) {
const rewriteHeader = opts.ctx.req.headers['x-nextjs-rewrite'] as
| string
| undefined;
const tenant = rewriteHeader?.split('tenant=')[1] ?? null;

let session: Session | null;
try {
session = await getServerSession(
opts.ctx.req,
opts.ctx.res,
// @ts-expect-error -- This will fail if called client side
getAuthOptions(opts.ctx.req),
);
} catch (error) {
session = await getSession(opts.ctx);
}

return {
session,
prisma,
tenant,
};
}

return {
session: null,
prisma,
tenant: null,
};
}
Mini
Mini11mo ago
why not just use middleware for page, check domain for tenant in create context?
DONUT
DONUT10mo ago
@haardik | LearnWeb3 Hi~~~ - Could you please share the details of getAuthOptions()? - If not, could you please tell me if it's not much different from using the regular AuthOptions in the next-auth?