tRPC-SvelteKit v3.6.1

End-to-end typesafe APIs for your SvelteKit applications

Using authentication

The tRPC-SvelteKit package works with both cookie-based and JWT-based authentication.

Below is an example of how you could secure your tRPC routes with cookie-based authentication.

Let's assume you have a username/password-based login page that POSTs to the following action:

routes/login/+page.server.ts
import { JWT_SECRET } from '$env/static/private';
import prisma from '$lib/prisma';
import { fail } from '@sveltejs/kit';
import { md5 } from 'hash-wasm';
import jwt from 'jsonwebtoken';
import type { Actions } from './$types';

export const actions: Actions = {
  default: async ({ request, cookies }) => {
    try {
      const data = await request.formData();
      const email = data.get('email') as string;
      const password = data.get('password') as string;

      // 👇 replace this with a non-naiive hashing algorithm
      const passwordHash = await md5(password);

      const { id, name } = await prisma.user.findFirstOrThrow({
        where: { email, passwordHash },
        select: { id: true, name: true }
      });

      cookies.set('jwt', jwt.sign({ id, name }, JWT_SECRET), { path: '/', secure: false });

      return { success: true };
      // 👆 or, if we're using HTTP headers based auth, we could return the token,
      // and let the client set the header on subsequent requests
    } catch {
      return fail(401, { error: 'Authentication failed' });
    }
  }
};

Your tRPC context builder could look like this:

lib/trpc/context.ts
import { JWT_SECRET } from '$env/static/private';
import type { RequestEvent } from '@sveltejs/kit';
import jwt from 'jsonwebtoken';

export async function createContext(event: RequestEvent) {
  try {
    const token = event.cookies.get('jwt');
    // 👆 or, if we're using HTTP headers based authentication, we could do something like this:
    // const token = event.request.headers.get('authorization')?.replace('Bearer ', '');

    const { id: userId } = jwt.verify(token || '', JWT_SECRET) as { id: string };

    return { userId };
  } catch {
    return { userId: '' };
  }
}

export type Context = Awaited<ReturnType<typeof createContext>>;

You could define an authentication middleware like this:

lib/trpc/middleware/auth.ts
import { t } from '$lib/trpc/t';
import { TRPCError } from '@trpc/server';

export const auth = t.middleware(async ({ next, ctx }) => {
  if (!ctx.userId) throw new TRPCError({ code: 'UNAUTHORIZED' });
  return next();
});

And you could use the auth middleware in your tRPC procedured as needed:

lib/trpc/routes/authors.ts
import prisma from '$lib/prisma';
import { auth } from '$lib/trpc/middleware/auth';
import { logger } from '$lib/trpc/middleware/logger';
import { t } from '$lib/trpc/t';
import { z } from 'zod';

export const authors = t.router({
  list: t.procedure
    .use(logger)
    .input(z.string().optional())
    .query(({ input }) =>
      prisma.author.findMany({
        select: {
          id: true,
          firstName: true,
          lastName: true,
          updatedAt: true,
          _count: { select: { books: true } }
        },
        orderBy: { updatedAt: 'desc' },
        where: input
          ? { OR: [{ firstName: { contains: input } }, { lastName: { contains: input } }] }
          : undefined
      })
    ),

  loadOptions: t.procedure.use(logger).query(() =>
    prisma.author
      .findMany({
        select: { id: true, firstName: true, lastName: true },
        orderBy: [{ firstName: 'asc' }, { lastName: 'asc' }]
      })
      .then((authors) =>
        authors.map(({ id, firstName, lastName }) => ({
          label: `${firstName} ${lastName}`,
          value: id
        }))
      )
  ),

  load: t.procedure
    .use(logger)
    .use(auth) // 👈 use auth middleware
    .input(z.string())
    .query(({ input }) =>
      prisma.author.findUniqueOrThrow({
        select: {
          id: true,
          firstName: true,
          lastName: true,
          bio: true,
          updatedAt: true,
          updatedBy: { select: { name: true } }
        },
        where: { id: input }
      })
    ),

  save: t.procedure
    .use(logger)
    .use(auth) // 👈 use auth middleware
    .input(
      z.object({
        id: z.string().nullable(),
        firstName: z.string().min(3).max(50),
        lastName: z.string().min(3).max(50),
        bio: z.string().nullable()
      })
    )
    .mutation(async ({ input: { id, ...rest }, ctx: { userId } }) => {
      if (id) {
        await prisma.author.update({
          data: { ...rest, updatedByUserId: userId },
          where: { id }
        });
      } else {
        await prisma.author.create({
          data: { ...rest, updatedByUserId: userId }
        });
      }
    }),

  delete: t.procedure
    .use(logger)
    .use(auth) // 👈 use auth middleware
    .input(z.string())
    .mutation(async ({ input: id }) => {
      await prisma.author.delete({ where: { id } });
    })
});