Skip to content

Integrating with tRPC

tRPC is an end-to-end typesafe API built in Typescript. This guide shows how to integrate Privy into any tRPC application.

There are two steps to enable auth in tRPC with Privy:

  • in your client, include the user's access token on requests
  • in your server, secure procedures by validating the token included on requests

TIP

If you're using tRPC with zod, check out this transformation tool to automatically generate zod schemas from Privy's types (e.g. user.email).

Configuring your client

When your client (frontend) makes a request to one of your tRPC procedures, you should include the Privy auth token, so that your server can verify that the user is authenticated.

INFO

The following works for both createTRPCProxyClient (vanilla) or createTRPCNextClient (Next.js). Note that while the configuration method signature is different between the two, the inner configuration object/strategy will remain the same. The example shown is for NextJS.

When scaffolding the tRPC client, pass the Privy auth token through the header of every request, via an httpBatchLink within the links configuration. Below is an example:

tsx
import {httpBatchLink} from '@trpc/client';
import {createTRPCNext} from '@trpc/next';

import {getAccessToken} from '@privy-io/react-auth';

export const api = createTRPCNext<AppRouter>({
  config() {
    return {
      links: [
        httpBatchLink({
          url: `your_base_url`,
          // apply the privy token to each request
          async headers() {
            return {
              Authorization: `Bearer ${(await getAccessToken()) || ''}`,
            };
          },
        }),
      ],
    };
  },
});

Protecting routes on your server

When your server receives a request from the client, it should validate the Privy auth token to confirm included in the request to ensure that it is authenticated.

First, parse the passed token using jose where you create your tRPC context:

typescript
import * as trpc from '@trpc/server';
import {inferAsyncReturnType} from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';

import {PrivyClient} from '@privy-io/server-auth';

// configure your privy server auth client

const privy = new PrivyClient(
  process.env.NEXT_PUBLIC_PRIVY_APP_ID || '',
  process.env.PRIVY_APP_SECRET || '',
);

export async function createContext({req, res}: trpcNext.CreateNextContextOptions) {
  const authToken = req.headers.authorization.replace('Bearer ', '');
  let userClaim: string | undefined = undefined;

  if (authToken) {
    try {
      userClaim = await privy.verifyAuthToken(token);
      // the claim contains all details about the validated privy token and can be passed
      // via the context for use in all server routes
      // if you want to pull additional details about the user via your api / db, such as whether the user is an
      // admin, here's your chance!
    } catch (_) {
      // this is an expected error for tRPC procedures that don't need to be authenticated
      // if privy is expected, we will throw a 403 at the middleware level, shown in the next step
    }
  }
  return {
    userClaim,
  };
}
export type Context = inferAsyncReturnType<typeof createContext>;

Next, create a middleware procedure for protecting routes:

typescript
const isPrivyAuthed = t.middleware(async ({ctx, next}) => {
  // check to make sure that the token was valid.
  // you can add further logic here, such as checking if the user is an admin,
  // if you added more user context within `createContext` above.
  if (!ctx.userClaim) {
    throw new TRPCError({
      code: 'UNAUTHORIZED',
      message: 'Not authenticated',
    });
  }
  return next({
    ctx,
  });
});
export const privyProtectedProcedure = t.procedure.use(isPrivyAuthed);

Finally, when defining routes, you can use your procedure middleware to ensure the user is properly authenticated.

typescript
t.router({
  // this is accessible for everyone
  hello: t.procedure
    .input(z.string().nullish())
    .query(({input, ctx}) => `hello ${input ?? ctx.user?.name ?? 'world'}`),
  admin: t.router({
    // this is accessible only to admins
    secret: privyProtectedProcedure.query(({ctx}) => {
      return {
        secret: 'sauce',
      };
    }),
  }),
});