boneyB
tRPC3y ago
5 replies
boney

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));
}


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,
  };
}


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:

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,
  };
}
Was this page helpful?