A full-stack monorepo starter with Bun, tRPC, Prisma, React, and shadcn/ui. Includes a working notes app as a reference implementation.
| Layer | Technology |
|---|---|
| Runtime | Bun |
| Monorepo | Turborepo |
| Linting | Biome |
| Database | PostgreSQL + Prisma ORM |
| API | tRPC v11 (HTTP standalone) |
| Frontend | React 19 + Vite + Tailwind CSS v4 |
| UI | shadcn/ui |
apps/
server/ – tRPC HTTP API server (Bun, port 3000)
web/ – Vite + React frontend (port 5173)
packages/
db/ – Prisma schema, generated client, PrismaClient singleton
services/ – Business logic (noteService, tagService); depends on db
trpc/
server/ – appRouter, routers, tRPC context; depends on services
client/ – Vanilla tRPC client factory for non-React consumers
ui/ – Shared shadcn/ui components, Tailwind CSS base styles
web (React)
└─ @workspace/trpc/client (tRPC react-query hooks)
└─ HTTP → apps/server
└─ @workspace/trpc/server (appRouter + routers)
└─ @workspace/services (business logic)
└─ @workspace/db (Prisma client)
└─ PostgreSQL
docker compose -f docker-compose.dev.yaml up -dThis starts a PostgreSQL container on localhost:5432 with user postgres, password postgres, and database lemony.
cp .env.example .envThe default values in .env.example match the dev Docker container and require no changes for local development.
Key variables:
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
postgresql://postgres:postgres@localhost:5432/lemony |
Prisma connection string |
PORT |
3000 |
API server port |
CORS_ORIGIN |
http://localhost:5173 |
Allowed origin for CORS |
VITE_SERVER_URL |
http://localhost:3000/trpc |
API URL used by the web app |
Note: All apps read from the single root
.envfile. Bun loads it automatically for the server (--env-filein the dev script); Vite reads it viaenvDir; Prisma CLI commands load it directly inprisma.config.ts.
bun installbun run db:push # sync schema to the dev database
bun run db:generate # regenerate the type-safe clientprisma db push is the recommended workflow for local development — it applies the current schema directly without creating migration files. Use prisma migrate dev when you want to track schema changes as versioned migrations (see Migrations).
bun run devTurborepo starts both apps/server and apps/web in parallel with hot-reload.
| Service | URL |
|---|---|
| API | http://localhost:3000 |
| Web app | http://localhost:5173 |
The production Docker Compose file builds the server into a minimal binary and runs Prisma migrations automatically before the server starts.
POSTGRES_PASSWORD=<your-secret> docker compose up --buildServices started:
- postgres — PostgreSQL database
- migrate — one-shot container that runs
prisma migrate deploy - server — compiled API server (starts after migrate completes)
The web app is not containerised by default. Deploy it to a static host (Vercel, Cloudflare Pages, etc.) and point
VITE_SERVER_URLat your production server URL at build time.
For schema changes that need to be tracked and deployed safely in production, use Prisma's migration workflow instead of db push:
# Create and apply a new migration (from workspace root)
bun run db:migrate -- --name <describe-your-change>
# Apply pending migrations in CI / production
bun run db:deployYou can also run Prisma CLI directly from packages/db — DATABASE_URL is loaded automatically from the workspace root .env:
cd packages/db
bunx prisma migrate dev --name <describe-your-change>
bunx prisma migrate deployComponents live in packages/ui/src/components and are shared across all apps.
cd packages/ui
bunx shadcn@latest add <component-name>Import anywhere in the monorepo:
import { Button } from "@workspace/ui/components/button"Follow these steps to add a new domain (e.g. Post):
Edit packages/db/schema.prisma, then sync and regenerate:
cd packages/db
bunx prisma db push
bunx prisma generateCreate packages/services/src/postService.ts:
import { prisma } from "@workspace/db";
export const postService = {
async list() {
return prisma.post.findMany({ orderBy: { createdAt: "desc" } });
},
// create, update, delete ...
};Export it from packages/services/src/index.ts:
export { postService } from "./postService";Create packages/trpc/server/routers/post.ts:
import { z } from "zod";
import { publicProcedure, router } from "../trpc";
import { postService } from "@workspace/services";
export const postRouter = router({
list: publicProcedure.query(() => postService.list()),
// ...
});Add it to packages/trpc/server/appRouter.ts:
import { postRouter } from "./routers/post";
export const appRouter = router({
note: noteRouter,
tag: tagRouter,
post: postRouter, // ← add this
});The new procedures are immediately available in the frontend via the typed trpc client with full autocompletion.
The tRPC Context (packages/trpc/server/trpc.ts) is intentionally empty in the starter. To add authentication:
- Populate
Contextwith your session/user data (e.g. from a JWT or cookie) - Pass the context factory to
createHTTPServerinapps/server/src/index.ts - Guard procedures using a
protectedProceduremiddleware that checksctx
| Command | Description |
|---|---|
bun run dev |
Start all dev servers |
bun run build |
Build all packages and apps |
bun run typecheck |
Type-check the entire monorepo |
bun run lint |
Lint the entire monorepo |
bun run format |
Format with Biome |
bun run check |
Lint + format + sort imports (Biome) |
bun run db:push |
Sync schema to dev database |
bun run db:migrate |
Create & apply a migration |
bun run db:generate |
Regenerate Prisma client |
cd packages/ui && bunx shadcn@latest add <c> |
Add a shadcn/ui component |