sveltekit-trpc Svelte Themes

Sveltekit Trpc

End-to-end type-safe full-stack app — SvelteKit 2.5, tRPC 11, Prisma, Lucia auth. Solves Prisma hot-reload connection exhaustion, tRPC context from locals, and query invalidation across load() and tRPC.

sveltekit-trpc

End-to-end type-safe full-stack app with SvelteKit, tRPC, and Prisma. Built in 2024 as a real alternative to Next.js + tRPC for teams that want Svelte's simplicity without giving up type safety across the client-server boundary.


Why This Exists

SvelteKit's server-side load functions and form actions already give you type safety on the server side. But the moment you need a flexible RPC pattern — batching, subscriptions, per-procedure auth — you need tRPC. The official trpc-sveltekit integration existed but was under-documented for production use in 2024.

This repo is the complete, production-annotated version of that integration, with the adapter gotchas solved.


Pain Points Solved

1. trpc-sveltekit forces Node adapter — no edge runtime

tRPC requires AsyncLocalStorage for context propagation, which isn't available on Cloudflare Workers or Vercel Edge. This means you're locked to @sveltejs/adapter-node or adapter-vercel with Node runtime. Fix: accept the constraint, document it, and use adapter-node with a multi-region deployment strategy instead of edge functions.

2. Prisma client singleton blowing up in SvelteKit dev mode

SvelteKit's Vite-based dev server hot-reloads modules, which re-instantiates PrismaClient on every change. Prisma warns about too many active connections and eventually crashes. Fix: use the singleton pattern in src/lib/server/db.ts — check globalThis.__prisma before creating a new client, only in development.

// src/lib/server/db.ts
const globalForPrisma = globalThis as unknown as { __prisma: PrismaClient }
export const prisma = globalForPrisma.__prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.__prisma = prisma

3. tRPC context not having access to SvelteKit's locals

The createContext function in tRPC doesn't automatically receive SvelteKit's event.locals (where your session lives). Fix: pass event from the SvelteKit handler into createContext, then extract the session from it — see src/lib/server/trpc.ts.

4. Type inference breaking when router is imported in +page.ts

Importing the tRPC router type on the client side pulls in server-only Prisma types, causing bundle bloat and Vite errors about Node built-ins. Fix: never import AppRouter directly in +page.ts files. Use the trpc client proxy — the inferred types flow through without importing server code.

5. invalidateQuery not triggering SvelteKit's reactive $page.data

tRPC's invalidateQuery doesn't interop with SvelteKit's invalidate(). After a mutation, $page.data loaded from a load function won't refresh automatically. Fix: call invalidate('trpc:all') from SvelteKit's invalidate API alongside trpc.utils.invalidate(), or migrate data fetching fully to tRPC queries.


Stack

Layer Technology
Framework SvelteKit 2.5
RPC tRPC 11
ORM Prisma 5.14 + PostgreSQL 16
Auth Lucia v3 (session-based)
Styling Tailwind CSS 3.4
Validation Zod 3.23
Adapter @sveltejs/adapter-node
Testing Vitest + Playwright

Project Structure

src/
├── lib/
│   ├── server/
│   │   ├── db.ts               # Prisma singleton
│   │   ├── trpc.ts             # tRPC init + context creation
│   │   └── routers/
│   │       ├── index.ts        # Root router — merges all sub-routers
│   │       ├── user.ts
│   │       └── post.ts
│   └── client/
│       └── trpc.ts             # Browser-side tRPC client proxy
├── routes/
│   ├── api/trpc/[...trpc]/     # tRPC HTTP handler
│   │   └── +server.ts
│   ├── auth/                   # Lucia auth routes
│   └── dashboard/
│       ├── +layout.server.ts   # Session guard
│       └── +page.svelte

prisma/
├── schema.prisma
└── migrations/

Getting Started

git clone https://github.com/yourusername/sveltekit-trpc
cd sveltekit-trpc
pnpm install

# Start PostgreSQL
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres:16

# Set up .env
echo 'DATABASE_URL="postgresql://postgres:postgres@localhost:5432/sveltekit_trpc"' > .env

# Run migrations and generate client
pnpm prisma migrate dev
pnpm prisma generate

# Dev server
pnpm dev

tRPC Procedure Example

// src/lib/server/routers/post.ts
export const postRouter = router({
  list: publicProcedure
    .input(z.object({ cursor: z.string().optional() }))
    .query(async ({ ctx, input }) => {
      return ctx.prisma.post.findMany({
        take: 20,
        cursor: input.cursor ? { id: input.cursor } : undefined,
        orderBy: { createdAt: 'desc' },
      })
    }),
  create: protectedProcedure
    .input(z.object({ title: z.string().min(1).max(200), body: z.string() }))
    .mutation(async ({ ctx, input }) => {
      return ctx.prisma.post.create({
        data: { ...input, authorId: ctx.session.userId },
      })
    }),
})

Known Limitations (2024)

  • tRPC subscriptions (WebSocket) require a separate server process — not supported in the SvelteKit Node adapter's default config
  • Prisma's Accelerate (global edge cache) is incompatible with this setup's adapter-node deployment model
  • SvelteKit's $effect rune (Svelte 5) interacts unexpectedly with tRPC's query invalidation — use $derived for reactive query data instead

License

MIT

Top categories

Loading Svelte Themes