A production-ready Turborepo monorepo starter template for building modern full-stack applications with Nuxt 4, Vue 3, Express/Nitro APIs, and PostgreSQL. Perfect for developers looking for a comprehensive TypeScript monorepo with component libraries, documentation, and production-grade infrastructure.
- Turborepo - Intelligent build system with caching and parallel execution
- Multiple Apps - Nuxt 4 frontend, Express API, Nitro server, Storybook, and Docus documentation
- Workspace Packages - Shared UI components, type definitions, utilities, and database schemas
- pnpm 10 - Fast, disk-efficient package manager with built-in security features
- Nuxt 4 + Vue 3 - Modern SSR framework with Composition API
- Tailwind CSS + DaisyUI - Utility-first styling with pre-built components
- Pinia - Type-safe state management
- Storybook - Component-driven development with isolated testing
- TypeScript - Full type safety across the entire stack
- Express API - Production-ready REST API with middleware, auth, and OpenAPI docs
- Nitro Server - Modern server framework with edge deployment support
- PostgreSQL + Drizzle ORM - Type-safe database with migrations and seed data
- Zod Validation - Runtime schema validation with auto-generated TypeScript types
- Docker Support - Multi-stage builds with BuildKit caching
- Corepack - Automatic package manager version management
- Security - Lifecycle script protection, minimum release age, rate limiting
- Structured Logging - Pino logger for request tracking and debugging
- API Documentation - Auto-generated OpenAPI/Swagger documentation
- Database Tools - Drizzle Kit migrations, PostgreSQL + pgAdmin containers
- Docus Documentation - Beautiful docs site powered by Nuxt Content
- Hot Module Replacement - Fast development with instant updates
- Shared TypeScript Configs - Consistent tsconfig across all packages
- ESLint + Prettier - Code quality and formatting enforcement
- CI/CD Ready - GitHub Actions workflows with prepare commands
Unlike minimal starters, this template provides production-grade infrastructure out of the box while remaining flexible enough to remove what you don't need. Choose between Express or Nitro for your API, use Storybook for component development, and deploy with Docker.
Built with pnpm 10's latest security features including lifecycle script protection and minimum release age settings. These features became critical after major npm supply chain attacks in 2025.
Leverages Turborepo for intelligent caching and parallel task execution. Shared packages (ui, helpers, api-types, db-schema, logger) demonstrate real-world monorepo patterns used in production applications.
- Nuxt 4.1+ - Latest stable with Vue 3.5+ and Vite 6
- TypeScript 5.7+ - Strict mode with comprehensive type safety
- pnpm 10 - Fastest package manager with built-in security
- Node 20+ LTS - Future-proof with long-term support
Includes Docus (powered by Nuxt 4 + Nuxt Content) for beautiful, searchable documentation. Markdown-based content with Vue component support makes it easy to maintain comprehensive docs alongside your code.
Storybook integration with pre-built UI components (Button, Input, Card, Modal, Alert, Badge) demonstrates component-driven development patterns. Perfect for design systems and reusable components.
PostgreSQL + Drizzle ORM setup with migrations, seeds, and type-safe queries. Docker Compose configuration includes pgAdmin for database management.
- Node.js 20+ (LTS) - Download
- Corepack - Package manager version manager (included with Node.js 16+)
- Docker (for database) - Get Docker
Enable Corepack if not already enabled:
corepack enable# Clone the repository
git clone https://github.com/your-org/turbo-nuxt-starter.git
cd turbo-nuxt-starter
# Install dependencies
pnpm install
# Rebuild native modules (required for Docus/better-sqlite3)
pnpm rebuild better-sqlite3
# Copy environment variables
cp .env.example .env
# Start PostgreSQL with Docker
docker-compose up -d postgres
# Run database migrations
cd packages/db-schema
pnpm migrate:push
cd ../..
# Start all apps
pnpm devThis starter includes Supabase authentication with local Docker development. No cloud account needed for development!
The supabase/ directory at the root contains the Supabase CLI configuration shared by all apps.
# Install Supabase CLI (if not already installed)
pnpm add -g supabase
# Start local Supabase stack from root (PostgreSQL, Auth, Storage, etc.)
supabase startAfter supabase start, copy the credentials to each app's .env file (apps/api/.env, apps/nitro/.env):
# Supabase Local Development
SUPABASE_URL=http://localhost:54321
# Option 1: New key system (2025+) - Recommended
# Displayed in `supabase start` output
SUPABASE_PUBLISHABLE_KEY=sb_publishable_*
# SUPABASE_SECRET_KEY=sb_secret_* # Only needed for admin operations
# Option 2: Legacy JWT keys - Still supported
# Get all credentials with: supabase status -o env
# SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs...
# JWT Secret for token verification (required for both key systems)
# Get from: supabase status -o env | grep JWT_SECRET
SUPABASE_AUTH_JWT_SECRET=<your-jwt-secret>
# Database (Supabase local uses port 54322, not 5432)
DATABASE_URL=postgresql://postgres:<password>@localhost:54322/postgres
POSTGRES_PORT=54322
POSTGRES_USER=postgres
POSTGRES_PASSWORD=<your-password>
POSTGRES_DB=postgresNote: The root .env.example contains only monorepo-level configuration (ports, NODE_ENV). App-specific configuration like Supabase keys should be in each app's .env file. See apps/api/.env.example and apps/nitro/.env.example for templates.
About Supabase Keys:
- API Keys (
sb_publishable_*oranon): For SDK initialization - functionally equivalent - JWT System: Newer asymmetric keys enable local token verification (faster, more secure)
- Both servers use
supabase.auth.getClaims()which automatically uses Web Crypto API with asymmetric keys - Learn more: Supabase API Keys | JWT Signing Keys
cd packages/db-schema
pnpm migrate:pushcd apps/api
pnpm seedThis creates 3 test accounts in Supabase Auth + database:
- alice@example.com / password123
- bob@example.com / password123
- charlie@example.com / password123
Supabase Studio (web UI) runs at http://localhost:54323
Manage users, view database, test Auth, and more!
- Sign up a user (creates Supabase Auth user + account record):
curl -X POST http://localhost:3002/api/auth/signup \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","password":"password123","fullName":"Alice Johnson"}'- Sign in (returns JWT access token):
curl -X POST http://localhost:3002/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","password":"password123"}'- Access protected endpoints (use JWT token):
curl http://localhost:3002/api/me \
-H "Authorization: Bearer <your-jwt-token>"- Supabase Auth handles passwords, JWT tokens, sessions
- Both APIs verify JWT tokens using
supabase.auth.getClaims()- With asymmetric keys: Local verification using Web Crypto API (fast, offline)
- With symmetric keys: Falls back to Auth server verification
- Account UUID matches Supabase Auth user ID (synced on signup)
- Protected routes require
Authorization: Bearer <token>header
# Check Supabase services status
supabase status
# Stop all services
supabase stop
# Reset database (WARNING: deletes all data)
supabase db reset
# Generate migration from schema changes
supabase db diff -f migration_nameThis Turborepo includes the following packages/apps:
vite- Vite + Vue 3 application (port 3000)nuxt- Nuxt 4 frontend application (port 3001)api- Express API server with TypeScript (port 3002)docus- Docus documentation site (port 3003)nitro- Nitro server example (port 3004)storybook- Storybook for component development (port 6006)ui- Shared Vue component library with Tailwind CSSeslint-config-custom- ESLint configurations for different environmentstsconfig- Shared TypeScript configurationstailwind-config- Shared Tailwind CSS configuration
Each package/app is 100% TypeScript.
This Turborepo has some additional tools already setup for you:
- TypeScript for static type checking
- ESLint for code linting
- Prettier for code formatting
Run all applications simultaneously:
pnpm devThis starts all applications on their configured ports:
| Application | Port | URL | Environment Variable |
|---|---|---|---|
| Vite + Vue 3 | 3000 | http://localhost:3000 | - |
| Nuxt Frontend | 3001 | http://localhost:3001 | NUXT_PORT |
| Express API | 3002 | http://localhost:3002 | API_PORT |
| Docus Docs | 3003 | http://localhost:3003 | DOCS_PORT |
| Nitro Server | 3004 | http://localhost:3004 | NITRO_PORT |
| Storybook | 6006 | http://localhost:6006 | - |
Infrastructure Services:
| Service | Port | URL | Environment Variable |
|---|---|---|---|
| Supabase Studio | 54323 | http://localhost:54323 | - |
| Supabase API | 54321 | http://localhost:54321 | SUPABASE_URL |
| PostgreSQL | 54322 | postgresql://localhost:54322 | POSTGRES_PORT |
Start specific services:
# Start only the API server
pnpm dev:api
# Start only the Nuxt frontend
pnpm dev:web
# Start Storybook
pnpm --filter storybook storybookBuild all applications and packages:
pnpm buildThis monorepo provides two production-ready API server options:
| Feature | Express (Port 3002) | Nitro (Port 3004) |
|---|---|---|
| Routing | Manual registration | File-based (automatic) |
| OpenAPI | @asteasolutions/zod-to-openapi |
Built-in defineRouteMeta |
| Deployment | Traditional servers | Edge-optimized, serverless |
| Ecosystem | Mature, extensive middleware | Modern, growing |
| Best For | Complex middleware chains | Fast prototyping, edge deployment |
Shared Foundation:
- Both use identical workspace packages (api-types, db-schema, helpers, logger)
- Same authentication flow (Supabase JWT verification)
- Identical response formats and error handling
- Complete database-backed CRUD operations with PostgreSQL + Drizzle ORM
Both servers provide the same core functionality with slightly different route paths:
| Method | Express Route | Nitro Route | Description |
|---|---|---|---|
| POST | /api/auth/signup |
/api/auth/signup |
Create new user account |
| POST | /api/auth/login |
/api/auth/login |
Authenticate and get JWT token |
| POST | /api/auth/logout |
/api/auth/logout |
Logout (protected) |
| Method | Express Route | Nitro Route | Description |
|---|---|---|---|
| GET | /api/me |
/api/me |
Get current user from database |
| GET | /api/users |
/user |
List all users |
| GET | /api/users/:id |
/user/:id |
Get user by ID |
| PATCH | /api/users/:id |
/user/:id |
Update user (self-update only) |
| DELETE | /api/users/:id |
/user/:id |
Delete user (self-delete only) |
| Method | Express Route | Nitro Route | Description |
|---|---|---|---|
| GET | /health |
/health |
Server health check |
| GET | /api/ |
- | API information |
Both servers provide interactive API documentation:
- Express Swagger UI: http://localhost:3002/docs
- Nitro Swagger UI: http://localhost:3004/\_swagger
- Nitro Scalar UI: http://localhost:3004/\_scalar
Both support the "Authorize" button for testing protected endpoints with JWT tokens.
curl -X POST http://localhost:3002/api/auth/signup \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "securePassword123",
"fullName": "John Doe"
}'curl -X POST http://localhost:3002/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "securePassword123"
}'Response includes JWT token:
{
"code": 200,
"data": {
"user": { "id": "...", "email": "..." },
"session": { "access_token": "eyJ...", "expires_in": 3600 },
"message": "Sign in successful"
}
}curl http://localhost:3002/api/me \
-H "Authorization: Bearer YOUR_JWT_TOKEN"curl http://localhost:3002/api/users \
-H "Authorization: Bearer YOUR_JWT_TOKEN"curl -X PATCH http://localhost:3002/api/users/USER_UUID \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"fullName": "Jane Doe Updated",
"email": "updated@example.com"
}'Authorization Pattern: Users can only update or delete their own accounts. Attempting to modify another user's account returns 403 Forbidden.
Both servers use PostgreSQL with Drizzle ORM for type-safe database operations:
- Connection: Singleton pattern with
getDb()from db-schema package - Schema: Shared
accountstable with uuid, email, fullName, createdAt - Migrations:
pnpm db:migrateapplies schema changes - Seeding:
pnpm db:seedpopulates test data - Studio:
pnpm db:studioopens Drizzle Studio for visual database management
Database connection details:
# PostgreSQL (via Supabase)
Host: localhost
Port: 54322
Database: postgres
User: postgres
Password: postgresThis monorepo includes complete Docker configurations for all applications with optimized multi-stage builds.
# Build and start all services
docker compose build
docker compose up -d
# Or combine into one command
docker compose up -d --build
# View logs
docker compose logs -f
# Stop all services
docker compose downAll applications have individual Dockerfiles with multi-stage builds:
| Service | Port | Dockerfile | Description |
|---|---|---|---|
| api | 3002 | apps/api/Dockerfile |
Express API server |
| nitro | 3004 | apps/nitro/Dockerfile |
Nitro server |
| nuxt | 3001 | apps/nuxt/Dockerfile |
Nuxt SSR frontend |
| vite | 3000 | apps/vite/Dockerfile |
Vite static app |
| docus | 3003 | apps/docus/Dockerfile |
Documentation site |
| storybook | 6006 | apps/storybook/Dockerfile |
Component library |
| postgres | 54322 | postgres:16-alpine |
PostgreSQL database |
docker-compose.yml - Production environment (default):
- Optimized production builds
- Health checks for all services
- Automatic restarts
- Minimal image sizes
- Supabase integration (no PostgreSQL needed)
docker-compose.db.yml - Optional local PostgreSQL:
- Use this for local development without Supabase
- Includes PostgreSQL 16 on port 54322
- Persistent volume for data storage
Note: For local development, use pnpm dev directly instead of Docker. Docker is primarily for production deployment testing.
Standard localhost access:
- Vite: http://localhost:3000
- Nuxt: http://localhost:3001
- API: http://localhost:3002
- Docus: http://localhost:3003
- Nitro: http://localhost:3004
- Storybook: http://localhost:6006
OrbStack users (automatic HTTPS domains): If you're using OrbStack instead of Docker Desktop, containers are automatically available at:
- Vite: https://vite.turbo-nuxt-starter.orb.local
- Nuxt: https://nuxt.turbo-nuxt-starter.orb.local
- API: https://api.turbo-nuxt-starter.orb.local
- Docus: https://docus.turbo-nuxt-starter.orb.local
- Nitro: https://nitro.turbo-nuxt-starter.orb.local
- Storybook: https://storybook.turbo-nuxt-starter.orb.local
OrbStack provides automatic local DNS and SSL certificates for all running containers.
# Build specific app
docker build -f apps/api/Dockerfile -t turbo-api .
# Build with BuildKit caching
DOCKER_BUILDKIT=1 docker build -f apps/api/Dockerfile -t turbo-api .# Build all services
docker compose build
# Start services in background
docker compose up -d
# Build and start (no cache)
docker compose build --no-cache && docker compose up -d
# View logs
docker compose logs -f api
# Stop all services
docker compose down
# Remove volumes
docker compose down -v
# Start with optional PostgreSQL database
docker compose -f docker-compose.yml -f docker-compose.db.yml up -dDocker services use environment variables from your .env file:
# Copy example environment file
cp .env.example .env
# Fill in required Supabase credentials
# Get from: supabase status -o env
SUPABASE_URL=http://localhost:54321
SUPABASE_ANON_KEY=your-anon-key
SUPABASE_AUTH_JWT_SECRET=your-jwt-secretNote: DATABASE_URL is the connection string used by application code. Individual POSTGRES_* vars configure the PostgreSQL container in docker-compose.db.yml.
Important: Docker containers cannot access localhost - it refers to the container itself, not your host machine.
- Local dev (pnpm dev): Use
localhostin URLs - Docker accessing host services: Replace
localhostwithhost.docker.internal - Production: Use your hosted Supabase project URL (e.g.,
https://your-project.supabase.co)
Example for Docker Compose accessing local Supabase:
SUPABASE_URL=http://host.docker.internal:54321
DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:54322/postgresAll Dockerfiles use:
- Multi-stage builds - Separate build and runtime stages
- BuildKit cache mounts - Faster dependency installation
- pnpm - Efficient monorepo package management
- Node 22 Alpine - Minimal base images (~50MB)
- Non-root users - Enhanced security
Production services include health checks:
# API health check (defined in Dockerfile)
healthcheck:
test:
[
"CMD",
"node",
"-e",
"require('http').get('http://localhost:3002/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
]
interval: 30s
timeout: 3s
retries: 3
start_period: 40sCheck service health:
docker compose psLocal Development (pnpm dev)
- Uses
tsxfor on-the-fly TypeScript execution - Reads workspace packages as TypeScript source files
- No build step required
- Hot reload enabled
Production/Docker (pnpm start)
- Requires transpiled output (
dist/directory) - Needs
node_moduleswith built workspace packages - Express API depends on external workspace packages
- Nuxt uses self-contained
.outputdirectory (includes bundled dependencies)
Why pnpm start fails locally:
The Express API (apps/api) uses workspace packages (logger, db-schema, etc.) as TypeScript source. When tsup transpiles the API, it doesn't bundle these workspace packages. Docker containers include node_modules from the installer stage, but running pnpm start locally without a full workspace build will fail with TypeScript import errors.
Issue: Build fails with "permission denied"
# Use BuildKit
export DOCKER_BUILDKIT=1
docker-compose buildIssue: Port already in use
# Change ports in .env file
API_PORT=3012
NUXT_PORT=3011Issue: Services can't connect to database
# Use Docker service name for DATABASE_URL
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgresturbo-nuxt-starter/
├── apps/
│ ├── api/ # Express.js API server
│ │ ├── src/
│ │ │ ├── routes/ # API routes
│ │ │ └── server.ts # Main server file
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── nuxt/ # Nuxt 3 frontend
│ ├── nitro/ # Nitro server example
│ ├── storybook/ # Component development
│ └── vite/ # Vite + Vue 3 app
├── packages/
│ ├── ui/ # Shared Vue components
│ ├── eslint-config-custom/ # ESLint configurations
│ ├── tsconfig/ # TypeScript configurations
│ └── tailwind-config/ # Tailwind CSS config
├── package.json # Root workspace config
└── turbo.json # Turborepo configuration
Copy .env.example to .env in the API directory:
cp apps/api/.env.example apps/api/.envConfigure as needed:
PORT- API server port (default: 3002)NODE_ENV- Environment (development/production)CORS_ORIGIN- CORS allowed origin
- Create route file in
apps/api/src/routes/ - Export router and import in
apps/api/src/routes/index.ts
- Add component to
packages/ui/ - Export from
packages/ui/index.ts - Use in any app:
import { MyComponent } from 'ui'
Add TypeScript types to packages/ui/types/ for cross-package usage.
Turborepo can use a technique known as Remote Caching to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines.
By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can create one, then enter the following commands:
cd my-turborepo
npx turbo login
This will authenticate the Turborepo CLI with your Vercel account.
Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo:
npx turbo link
Nuxt and Nitro apps generate their TypeScript configurations at prepare time:
apps/nuxt/.nuxt/tsconfig.json- Generated bynuxt prepareapps/nitro/.nitro/tsconfig.json- Generated bynitro prepareapps/docus/.nuxt/tsconfig.json- Generated bynuxt prepare --extends docus
Local Development:
- These are generated automatically via
postinstallscripts when you runpnpm install - No manual action required
CI/CD Pipelines:
- If you skip postinstall scripts (common practice with
--ignore-scripts), you must runturbo preparebefore typechecking - This ensures all
.nuxtand.nitrodirectories are generated with proper TypeScript configurations
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 10.17.1
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
# Install dependencies (skip postinstall for speed)
- run: pnpm install --frozen-lockfile --ignore-scripts
# Rebuild native modules
- run: pnpm rebuild better-sqlite3
# Generate TypeScript configs for Nuxt/Nitro
- run: turbo prepare
# Run quality checks
- run: turbo lint:check
- run: turbo typecheck
- run: turbo test:unit
- run: turbo buildIf you need to manually regenerate TypeScript configurations:
# Regenerate all configs
turbo prepare
# Or for a specific app
pnpm --filter nuxt prepare
pnpm --filter nitro prepare
pnpm --filter docus prepareLearn more about the power of Turborepo:
If pnpm typecheck fails with errors about missing TypeScript configurations or cannot find auto-imported functions (useHead, defineNuxtConfig, etc.):
Cause: The .nuxt and .nitro directories haven't been generated yet.
Solution:
# Regenerate TypeScript configurations
turbo prepare
# Then run typecheck again
pnpm typecheckThis is especially common in CI/CD environments when using pnpm install --ignore-scripts. See the CI/CD Setup section for more details.
If you encounter an error like Could not locate the bindings file when running the Docus app, it means the native module better-sqlite3 needs to be rebuilt for your system.
Error example:
Error: Could not locate the bindings file. Tried:
→ /path/to/node_modules/better-sqlite3/build/better_sqlite3.node
Solution:
# Rebuild the native module
pnpm rebuild better-sqlite3This is required because better-sqlite3 is a native Node.js addon that must be compiled for your specific:
- Node.js version (e.g., Node 22.17.0)
- Operating system (macOS, Linux, Windows)
- CPU architecture (arm64, x64)
The rebuild command will compile the module correctly for your environment.
This starter uses pnpm 10 with enhanced security features to protect against supply chain attacks.
By default, lifecycle scripts from dependencies are disabled. This prevents malicious packages from executing arbitrary code during installation.
Only explicitly allowed packages can run lifecycle scripts, configured in pnpm-workspace.yaml:
onlyBuiltDependencies:
- better-sqlite3 # Native module requiring compilation
- esbuild # Build tool needing platform binariesIf you need to allow additional packages to run lifecycle scripts, add them to this list.
New package versions are delayed by 24 hours before installation to give the community time to identify malicious releases.
minimumReleaseAge: 1440 # 24 hours in minutesThis setting is configured in pnpm-workspace.yaml and helps protect against:
- Compromised maintainer accounts
- Malicious package updates
- Supply chain injection attacks
Note: This only affects newly published versions. Existing versions install immediately.
Corepack automatically manages the pnpm version based on the packageManager field in package.json:
{
"packageManager": "pnpm@10.17.1"
}This ensures everyone on your team uses the same pnpm version, eliminating "works on my machine" issues.