u/Western_Door6946

Help me understand if my current setup is correct and lmk what I'm doing wrong
▲ 2 r/Supabase+1 crossposts

Help me understand if my current setup is correct and lmk what I'm doing wrong

I am currently learning how the technologies mentioned below work together. My current setup is Turborepo, Next.JS, Supabase, Supabase Auth, drizzle, tRPC v11, tanstack-query.

I think I'm missing something and I have a few questions.

  1. From what I understand I only need to create a client if my api would be in the apps folder (used by hono or express). In that case I should use supabase.auth.getSession() in apps/web, get the access token, add it in headers and send it to the apps/api, correct? By doing this, I would verify that the client making the request is the same with the one that is on the apps/api.

  2. Am I using supabase.auth.getClaims() correctly? Am I calling this function too many times? I'm suppose it's ok to use getClaims() since I am not using a separate app for the API.

In total, I am using supabase.auth.getClaims() in 3 places: server.tsx, trpc/[trpc]/route.ts, auth/callback/route.ts, and a 4th place would be in proxy.ts once I write it. Is this setup correct?

If someone can take the time to look over the code and clarify when getClaims() should be used and why and why in a separate app it is important to send an "access token" in headers, I would much appreciate it. This is all a bit confusing to me.

monorepo/
├── apps/
│   └── web/                         # Next.js app
│       ├── app/
│       │   ├── api/
│       │   │   └── trpc/
│       │   │       └── route.ts    # tRPC HTTP handler
│       │   ├── auth/
│       │   │   └── callback/
│       │   │       └── route.ts    # Fixed auth callback
│       │   └── layout.tsx          # Wraps TRPCReactProvider
│       ├── lib/
│       │   └── trpc/
│       │       ├── client.tsx      # TRPCReactProvider
│       │       ├── server.tsx      # trpc proxy for RSC
│       │       └── query-client.ts
│       └── utils/
│           └── constants.ts
├── packages/
│   ├── db/                          # Drizzle + postgres
│   │   ├── client.ts                # db
│   │   └── instrument.ts
│   ├── trpc/
│   │   ├── init.ts                  # t, createTRPCContext, middleware
│   │   └── routers/
│   │       ├── _app.ts              # appRouter
│   │       ├── users.ts
│   │       └── ...
│   ├── supabase/
│   │   └── server.ts                # createClient
│   └── utils/
│       └── sanitize-redirect.ts
└── package.json

Here are my files:
packages\trpc\src\init.ts:

import type { Database } from "@monorepo/db/client";
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";

export type AuthClaims = {
  sub: string;
  email?: string;
  role?: string;
  app_metadata?: Record<string, unknown>;
  user_metadata?: Record<string, unknown>;
  [key: string]: unknown;
};

export type TRPCContext = {
  headers: Headers;
  claims: AuthClaims | null;
  db: Database;
};

/**
 * This context creator accepts `headers` so it can be reused in both
 * the RSC server caller (where you pass `next/headers`) and the
 * API route handler (where you pass the request headers).
 */
export const createTRPCContext = async (opts: {
  headers: Headers;
  claims: AuthClaims | null;
  db: Database;
}): Promise<TRPCContext> => {
  
// const user = await auth(opts.headers);
  return {
    headers: opts.headers,
    claims: opts.claims,
    db: opts.db,
  };
};

// Avoid exporting the entire t-object
// since it's not very descriptive.
// For instance, the use of a t variable
// is common in i18n libraries.
const t = initTRPC.context<Awaited<ReturnType<typeof createTRPCContext>>>().create({
  
/**
   * u/see https://trpc.io/docs/server/data-transformers
   */
  transformer: superjson,
});

const enforceAuth = t.middleware(async ({ ctx, next }) => {
  if (!ctx.claims) {
    throw new TRPCError({
      code: "UNAUTHORIZED",
      message: "You must be signed in to perform this action",
    });
  }

  return next({
    ctx: {
      ...ctx,
      claims: ctx.claims,
    },
  });
});


// Base router and procedure helpers
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(enforceAuth);

apps\web\src\trpc\server.tsx:

// tRPC caller for Server Components
// To prefetch queries from server components, we create a proxy from our router.
// You can also pass in a client if your router is on a separate server.

import "server-only"; 
// <-- ensure this file cannot be imported from the client
import { cache } from "react";
import { headers } from "next/headers";
import { db } from "@monorepo/db/client";
import { createClient } from "@monorepo/supabase/server";
import { createTRPCContext } from "@monorepo/trpc/init";
import type { AppRouter } from "@monorepo/trpc/routers/_app";
import { appRouter } from "@monorepo/trpc/routers/_app";
import { createTRPCOptionsProxy, type TRPCQueryOptions } from "@trpc/tanstack-react-query";
import { makeQueryClient } from "./query-client";


// IMPORTANT: Create a stable getter for the query client that
//            will return the same client during the same request.
export const getQueryClient = cache(makeQueryClient);

export const trpc = createTRPCOptionsProxy<AppRouter>({
  ctx: async () => {
    const supabase = await createClient();
    const { data } = await supabase.auth.getClaims();

    return createTRPCContext({
      headers: await headers(),
      claims: data?.claims ?? null,
      db,
    });
  },
  router: appRouter,
  queryClient: getQueryClient,
});

// If your router is on a separate server, pass a client instead:
// createTRPCOptionsProxy({
//   client: createTRPCClient({ links: [httpLink({ url: '...' })] }),
//   queryClient: getQueryClient,
// });

export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(queryOptions: T) {
  const queryClient = getQueryClient();

  if (queryOptions.queryKey[1]?.type === "infinite") {
    void queryClient.prefetchInfiniteQuery(queryOptions as any).catch(() => {
      
// Avoid unhandled promise rejections from fire-and-forget prefetches.
    });
  } else {
    void queryClient.prefetchQuery(queryOptions).catch(() => {
      
// Avoid unhandled promise rejections from fire-and-forget prefetches.
    });
  }
}


export function batchPrefetch<T extends ReturnType<TRPCQueryOptions<any>>>(queryOptionsArray: T[]) {
  const queryClient = getQueryClient();

  for (const queryOptions of queryOptionsArray) {
    if (queryOptions.queryKey[1]?.type === "infinite") {
      void queryClient.prefetchInfiniteQuery(queryOptions as any).catch(() => {
        
// Avoid unhandled promise rejections from fire-and-forget prefetches.
      });
    } else {
      void queryClient.prefetchQuery(queryOptions).catch(() => {
        
// Avoid unhandled promise rejections from fire-and-forget prefetches.
      });
    }
  }
}

apps\web\src\app\api\trpc\[trpc]\route.ts:

import { db } from "@monorepo/db/client";
import { createClient } from "@monorepo/supabase/server";
import { createTRPCContext } from "@monorepo/trpc/init";
import { appRouter } from "@monorepo/trpc/routers/_app";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: async () => {
      const supabase = await createClient();
      const { data } = await supabase.auth.getClaims();

      return createTRPCContext({
        headers: req.headers,
        claims: data?.claims ?? null,
        db,
      });
    },
  });


export { handler as GET, handler as POST };

Now here is the auth, which I am not sure is correct. Here I am trying to verify the user role which is just a column in public.users table. Should I use tRPC here or just query the database directly?
apps\web\src\app\api\auth\callback\route.ts:

import { cookies } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { db } from "@monorepo/db/client";
import { getUserById, getUserInvitesByEmail } from "@monorepo/db/queries";
import { createClient } from "@monorepo/supabase/server";
import { sanitizeRedirectPath } from "@monorepo/utils/sanitize-redirect";
import { addYears } from "date-fns";
import { Cookies } from "@/lib/utils/constants";

export async function GET(req: NextRequest) {
  const cookieStore = await cookies();
  const requestUrl = new URL(req.url);
  const code = requestUrl.searchParams.get("code");
  const returnTo = requestUrl.searchParams.get("return_to");
  const provider = requestUrl.searchParams.get("provider");

  if (provider) {
    cookieStore.set(Cookies.PreferredSignInProvider, provider, {
      expires: addYears(new Date(), 1),
    });
  }

  if (code) {
    const supabase = await createClient();
    await supabase.auth.exchangeCodeForSession(code);

    const { data } = await supabase.auth.getClaims();
    const user = data?.claims;

    if (user) {
      const userId = user.sub;
      const userEmail = user.email;

      const userData = await getUserById(db, userId);

      const userRole = userData?.role;

      
// Check if user has pending invitations when role is null
      if (userRole === null) {
        const inviteData = await getUserInvitesByEmail(db, userEmail!);

        // If there's an invitation, send them to teams page
        if (inviteData) {
          return NextResponse.redirect(`${requestUrl.origin}/team`);
        }

        
// Otherwise proceed with setup
        return NextResponse.redirect(`${requestUrl.origin}/setup`);
      }

      if (userRole === "admin" || userRole === "teacher") {
        return NextResponse.redirect(`${requestUrl.origin}/dashboard`);
      }

      if (userRole === "student" || userRole === "parent") {
        return NextResponse.redirect(`${requestUrl.origin}/profile`);
      }
    }
  }

  if (returnTo) {
    
// The middleware strips the leading "/" (e.g. "settings/accounts"),
    
// but sanitizeRedirectPath requires a root-relative path starting with "/".
    const normalized = returnTo.startsWith("/") ? returnTo : `/${returnTo}`;
    const safePath = sanitizeRedirectPath(normalized);
    return NextResponse.redirect(`${origin}${safePath}`);
  }

  return NextResponse.redirect(requestUrl.origin);
}

This is the tRPC client:

"use client";
// ^-- to make sure we can mount the Provider from a server component
import { useState } from "react";
import type { AppRouter } from "@monorepo/trpc/routers/_app";
import type { QueryClient } from "@tanstack/react-query";
import { QueryClientProvider } from "@tanstack/react-query";
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import { createTRPCContext } from "@trpc/tanstack-react-query";
import superjson from "superjson";
import { makeQueryClient } from "./query-client";

export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();

let browserQueryClient: QueryClient;
function getQueryClient() {
  if (typeof window === "undefined") {
    
// Server: always make a new query client
    return makeQueryClient();
  }
  
// Browser: make a new query client if we don't already have one
  
// This is very important, so we don't re-make a new client if React
  
// suspends during the initial render. This may not be needed if we
  
// have a suspense boundary BELOW the creation of the query client
  if (!browserQueryClient) browserQueryClient = makeQueryClient();

  return browserQueryClient;
}


function getUrl() {
  const base = (() => {
    if (typeof window !== "undefined") return "";
    if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;

    return "http://localhost:3000";
  })();


  return `${base}/api/trpc`;
}


export function TRPCReactProvider({ children }: { children: React.ReactNode }) {
  
// NOTE: Avoid useState when initializing the query client if you don't
  
//       have a suspense boundary between this and the code that may
  
//       suspend because React will throw away the client on the initial
  
//       render if it suspends and there is no boundary
  const queryClient = getQueryClient();

  const [trpcClient] = useState(() =>
    createTRPCClient<AppRouter>({
      links: [
        httpBatchLink({
          transformer: superjson, 
// <-- if you use a data transformer
          url: getUrl(),
        }),
      ],
    }),
  );


  return (
    <QueryClientProvider 
client
={queryClient}>
      <TRPCProvider 
trpcClient
={trpcClient} 
queryClient
={queryClient}>
        {children}
      </TRPCProvider>
    </QueryClientProvider>
  );
}

packages\trpc\src\server\routers\users.ts:

import { getUserById } from "@monorepo/db/queries";
import { createTRPCRouter, protectedProcedure } from "../../init";

export const usersRouter = createTRPCRouter({
  me: protectedProcedure.query(async ({ ctx: { db, claims } }) => {
    const user = await getUserById(db, claims.sub);

    return user;
  }),
});
u/Western_Door6946 — 19 hours ago