T
tRPC

❓-help

Including multi-tenant config into tRPC context

HLhaardik | LearnWeb31/2/2024
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
Solution
HLhaardik | LearnWeb31/2/2024
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,
};
}
MMini1/9/2024
why not just use middleware for page, check domain for tenant in create context?
DDONUT2/9/2024
@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?

Looking for more? Join the community!

Recommended Posts
Suggestion to deal with external dependencies on backend side?Hello, i have a question about dealing with external dependencies inside mutations and queries on tTRPC & Zod LoggingI'm building a backend and if my input validation fails I want to log the failure but couldn't figurHow to handle Query Errors on client side (show toast on 401)Hey ! Hope you're doing well ! I wanted to know if someone can explain to me how to handle errors onTRPCClientError: JSON Parse error: Single quotes (') are not allowed in JSONI am getting this error when deployed but not when running locally. ```json TRPCClientError: JSON PHow to access isLoading if a Query is inside a componentI am having trouble in accessing isLoading from a query, the problem is that the Query is placed instRPC and vite serverless, are there any exmaples?Now that tanstack router is out I would want to move out of nextjs, but keep the convenience of hostHow can i use inferprocedureoutput?I'd like to write this code type-safely:```ts const handleQuery = (query: /* ? */) => {} const MyCoCan a URL be formatted with dashes, such as "user-list" in the context of trpc?Can a URL be formatted with dashes, such as "user-list" in the context of trpc? ``` const appRouter Incorrect type inference with discriminated unions.Hey folks, I am having a really weird issue. Here is a minimal reproducible example: **https://tsplGet Query Key on ServerI'm following some recommended practices from one of the react query maintainers `@tkdodo` and want ReferenceError: Cannot access 'appRouter' before initializationHi friends im having trouble implementing a "protected" or "private" procedure in TRPC using Next.jstrpc openapi does not work on app routerhttps://github.com/jlalmes/trpc-openapi Can i just know if trpc open api for next is also applicable