Monorepo template for all projects. Source of truth for shared patterns, tooling, and configurations.
- Framework: Next.js 16, Hono
- Language: TypeScript (strict)
- Styling: Tailwind CSS v4, Radix UI, shadcn/ui patterns
- Database: Prisma 7, PostgreSQL
- Auth: Better Auth
- Monorepo: Turborepo, pnpm workspaces
- Linting: oxlint
- Formatting: oxfmt
- Testing: Vitest (unit), Playwright (e2e)
| App | Description | Dev URL |
|---|---|---|
web |
Main application | https://acme.web.localhost |
api |
Hono backend API | https://acme.api.localhost |
landing |
Marketing site | https://acme.landing.localhost |
| Package | Description |
|---|---|
@repo/ui |
Shared React component library |
@repo/db |
Prisma database client |
@repo/auth |
Authentication module |
@repo/typescript-config |
Shared TypeScript configs |
@repo/config-vitest |
Shared Vitest test configs |
- Node.js 24 (use
nvm install 24 && nvm use 24) - pnpm 11 (
npm install -g pnpm@11) - PostgreSQL running locally on
:5432(or use Docker, see below) - portless for stable HTTPS dev URLs (see step 2)
pnpm installDev servers run behind portless, which gives each app a stable https://*.localhost URL instead of a random port. This is required: the dev scripts wrap each app in portless run …, and Better Auth's secure cookies + CORS allowlists assume the portless hostnames.
One-time per machine:
npm install -g portless
sudo portless proxy start --https # binds :443, trusts the local certThe proxy auto-restarts on subsequent boots once trusted.
If you don't already have Postgres running:
docker run -d --name acme-pg \
-e POSTGRES_USER=acme \
-e POSTGRES_PASSWORD=acme123 \
-e POSTGRES_DB=acme \
-p 5432:5432 \
postgres:16Each package loads env vars from its own directory. Copy each example and edit as needed:
cp apps/api/.env.example apps/api/.env
cp apps/landing/.env.example apps/landing/.env
cp apps/web/.env.example apps/web/.env
cp packages/db/.env.example packages/db/.env(apps/landing needs its file only at build time: NEXT_PUBLIC_WEB_APP_URL is
inlined into the client bundle by next build and read nowhere at runtime. Its
default already points at the local web app, so pnpm dev works without it.)
Then edit each file and set:
BETTER_AUTH_SECRET: any 32+ char random string, identical acrossapps/api/.envandapps/web/.env(both validate sessions against it). Generate withopenssl rand -base64 32.DATABASE_URL: replace the name and password placeholders.
The URL variables (NEXT_PUBLIC_API_URL, BETTER_AUTH_URL, CORS_ORIGINS, TRUSTED_ORIGINS) are pre-set to the portless URLs and don't need changes for local dev.
pnpm db:generate # generate the Prisma client
pnpm db:push # apply the schema to your database
pnpm db:seed # optional: seed sample datapnpm devOpen:
- Web: https://acme.web.localhost
- Landing: https://acme.landing.localhost
- API: https://acme.api.localhost
- OpenAPI docs (Scalar): https://acme.api.localhost/docs
- Schema JSON: https://acme.api.localhost/openapi.json
Branch name auto-prefixes the subdomain, so concurrent worktrees don't collide:
main worktree: https://acme.web.localhost
branch fix-styles: https://fix-styles.acme.web.localhost
| Command | Description |
|---|---|
pnpm dev |
Start all apps in development |
pnpm build |
Build all apps and packages |
pnpm test |
Run unit tests |
pnpm test:e2e |
Run Playwright e2e tests |
pnpm lint |
Run oxlint |
pnpm format |
Format with oxfmt |
pnpm format:check |
Check formatting |
pnpm typecheck |
Run TypeScript checks |
pnpm db:generate |
Generate Prisma client |
pnpm db:push |
Push schema to database |
pnpm db:seed |
Seed database |
pnpm clean |
Clean all build artifacts |