Back to Blog
Tutorial10 min read2026-07-04

How to Deploy a tRPC Full-Stack App

tRPC gives you end-to-end type safety with no codegen, but deploying it means thinking about how client and server ship together. This tutorial covers monorepo and Next.js setups, plus the database wiring.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

# How to Deploy a tRPC Full-Stack App

tRPC is a delight to develop with: you define procedures on the server and call them from the client with full TypeScript inference, no schemas, no codegen. The catch comes at deploy time — tRPC's magic depends on client and server sharing types from the same codebase, which shapes how you build and ship. This tutorial covers deploying a tRPC app cleanly.

Two common topologies

tRPC apps usually take one of two shapes:

  1. 1All-in-one (Next.js / similar). The tRPC router runs inside the same app as the frontend (e.g. a Next.js route handler). One deployable unit. Simplest to ship.
  2. 2Separate client + server in a monorepo. A standalone tRPC server (Node HTTP/Express/Fastify) and a separate frontend, sharing a types package. Two deployable units, more flexible.

Your choice changes the deploy, so decide early.

The shared-types reality

The core thing to internalize: the client imports the AppRouter type from the server. This is type-only — it is erased at build time and ships no server code to the browser:

// client
import type { AppRouter } from '../server/router';
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';

export const trpc = createTRPCProxyClient<AppRouter>({
  links: [httpBatchLink({ url: process.env.NEXT_PUBLIC_API_URL + '/trpc' })],
});

For this import to resolve at build time, the client build must have access to the server's source/types — trivial in a monorepo or a single Next.js app, but something to plan for if you split repos.

Option A: Next.js all-in-one

The simplest deploy. The router lives in an API route, the frontend calls it, everything builds and deploys together.

// server side: the router
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
import { db } from './db';

const t = initTRPC.create();
export const appRouter = t.router({
  getTodos: t.procedure.query(() => db.todo.findMany()),
  addTodo: t.procedure
    .input(z.object({ title: z.string().min(1) }))
    .mutation(({ input }) => db.todo.create({ data: input })),
});
export type AppRouter = typeof appRouter;

Deploy the Next.js app as a single container. The API and UI ship together, so there is no CORS and no version-skew between client and server types — they are always from the same build.

Option B: standalone server + separate client

When you want the API independent (mobile clients, multiple frontends), run a standalone tRPC server:

import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './router';

createHTTPServer({
  router: appRouter,
  createContext: ({ req }) => ({ user: authenticate(req) }),
}).listen(Number(process.env.PORT) || 8080);

Now you deploy two units: the API container and the static/SSR frontend. You must:

  • Configure CORS on the server for the frontend origin.
  • Keep the shared types package versioned so client and server stay in sync.
  • Point the client at the deployed API URL via a build-time env var.

Step: wire the database

tRPC procedures call your data layer. Read the connection string from the environment, exactly as any Node app:

// db.ts (Prisma example)
import { PrismaClient } from '@prisma/client';
export const db = new PrismaClient();
// Prisma reads DATABASE_URL from the environment automatically

Run migrations as a release step before traffic arrives:

npx prisma migrate deploy

Step: containerize

FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
ENV PORT=8080
EXPOSE 8080
CMD ["npm", "start"]

In a monorepo, make sure the build context includes the shared packages the build needs.

Deployment comparison

All-in-one (Next.js)Separate client + server
Deployable units12
CORSNoneRequired
Type sharingTrivialVia shared package
Best forWeb-only appsMultiple/mobile clients
ComplexityLowMedium

Common pitfalls

  • Importing server code instead of just the type. Use import type so no server logic leaks into the client bundle.
  • Type skew across split deploys. If client and server deploy independently with mismatched types, you lose tRPC's safety guarantee at runtime. Version the shared package and deploy together.
  • Forgetting prisma migrate deploy. Procedures fail with missing-table errors.
  • Missing CORS in option B. Browser requests blocked.

Deploying on PandaStack

PandaStack auto-detects Node/Next.js apps or builds from your Dockerfile, so either topology deploys from a git push. For the all-in-one Next.js app, it is a single container deploy. For the split setup, deploy the tRPC server as a web service and the frontend as a static site or container. Provision a managed PostgreSQL and DATABASE_URL is injected automatically, so Prisma connects with zero extra config; you can run prisma migrate deploy as part of your build. Custom domains get automatic SSL, and live logs plus rollbacks make iterating on procedures painless. The free tier (5 web services + 5 static sites + 1 database) covers both topologies.

References

  • [tRPC documentation](https://trpc.io/docs)
  • [tRPC standalone adapter](https://trpc.io/docs/server/adapters/standalone)
  • [Prisma migrate deploy](https://www.prisma.io/docs/orm/prisma-migrate/workflows/development-and-production)
  • [Next.js deployment docs](https://nextjs.org/docs/app/building-your-application/deploying)

---

Deploying tRPC is mostly about keeping client and server types in lockstep. PandaStack builds either topology from one git push and auto-wires your database — start free at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also