Skip to content

JDIZM/turbo-nuxt-starter

Repository files navigation

Turbo Nuxt Starter - Turborepo + Nuxt 4 + Vue 3 Monorepo

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.

🚀 Features

Full-Stack Monorepo Architecture

  • 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

Frontend Excellence

  • 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

Backend Flexibility

  • 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

Production-Ready Infrastructure

  • 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

Developer Experience

  • 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

Why This Starter?

🎯 Comprehensive Yet Flexible

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.

🔒 Security-First Approach

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.

📦 True Monorepo Architecture

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.

🚀 Modern Tech Stack

  • 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

📚 Documentation-Driven

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.

🎨 Component Library Ready

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.

🗄️ Database-Ready

PostgreSQL + Drizzle ORM setup with migrations, seeds, and type-safe queries. Docker Compose configuration includes pgAdmin for database management.

🚀 Quick Start

Prerequisites

  • 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

Installation

# 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 dev

🔐 Supabase Local Development

This 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.

Start Supabase

# Install Supabase CLI (if not already installed)
pnpm add -g supabase

# Start local Supabase stack from root (PostgreSQL, Auth, Storage, etc.)
supabase start

Configure Environment

After 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=postgres

Note: 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_* or anon): 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

Run Database Migrations

cd packages/db-schema
pnpm migrate:push

Seed Test Accounts (Optional)

cd apps/api
pnpm seed

This creates 3 test accounts in Supabase Auth + database:

Access Supabase Studio

Supabase Studio (web UI) runs at http://localhost:54323

Manage users, view database, test Auth, and more!

Authentication Flow

  1. 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"}'
  1. 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"}'
  1. Access protected endpoints (use JWT token):
curl http://localhost:3002/api/me \
  -H "Authorization: Bearer <your-jwt-token>"

How Auth Works

  • 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

Useful Commands

# 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_name

What's inside?

This Turborepo includes the following packages/apps:

Apps and Packages

  • 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 CSS
  • eslint-config-custom - ESLint configurations for different environments
  • tsconfig - Shared TypeScript configurations
  • tailwind-config - Shared Tailwind CSS configuration

Each package/app is 100% TypeScript.

Utilities

This Turborepo has some additional tools already setup for you:

Development

Start All Services

Run all applications simultaneously:

pnpm dev

This 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

Individual Services

Start specific services:

# Start only the API server
pnpm dev:api

# Start only the Nuxt frontend
pnpm dev:web

# Start Storybook
pnpm --filter storybook storybook

Build

Build all applications and packages:

pnpm build

Dual Server Architecture

This 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

API Endpoints

Both servers provide the same core functionality with slightly different route paths:

Authentication Endpoints (Public)

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)

User Endpoints (Protected - Requires JWT)

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)

Health & Info Endpoints

Method Express Route Nitro Route Description
GET /health /health Server health check
GET /api/ - API information

OpenAPI Documentation

Both servers provide interactive API documentation:

Both support the "Authorize" button for testing protected endpoints with JWT tokens.

Example API Requests

Signup

curl -X POST http://localhost:3002/api/auth/signup \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "securePassword123",
    "fullName": "John Doe"
  }'

Login

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"
  }
}

Get Current User (Protected)

curl http://localhost:3002/api/me \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

List Users (Protected)

curl http://localhost:3002/api/users \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

Update User (Protected - Self-Update Only)

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.

Database Integration

Both servers use PostgreSQL with Drizzle ORM for type-safe database operations:

  • Connection: Singleton pattern with getDb() from db-schema package
  • Schema: Shared accounts table with uuid, email, fullName, createdAt
  • Migrations: pnpm db:migrate applies schema changes
  • Seeding: pnpm db:seed populates test data
  • Studio: pnpm db:studio opens Drizzle Studio for visual database management

Database connection details:

# PostgreSQL (via Supabase)
Host: localhost
Port: 54322
Database: postgres
User: postgres
Password: postgres

🐳 Docker Deployment

This monorepo includes complete Docker configurations for all applications with optimized multi-stage builds.

Quick Start

# 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 down

Available Docker Services

All 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 Files

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.

Accessing Services

Standard localhost access:

OrbStack users (automatic HTTPS domains): If you're using OrbStack instead of Docker Desktop, containers are automatically available at:

OrbStack provides automatic local DNS and SSL certificates for all running containers.

Building Individual Images

# 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 .

Common Docker Commands

# 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 -d

Environment Variables

Docker 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-secret

Note: DATABASE_URL is the connection string used by application code. Individual POSTGRES_* vars configure the PostgreSQL container in docker-compose.db.yml.

Docker Networking

Important: Docker containers cannot access localhost - it refers to the container itself, not your host machine.

  • Local dev (pnpm dev): Use localhost in URLs
  • Docker accessing host services: Replace localhost with host.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/postgres

Docker Image Optimization

All 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

Health Checks

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: 40s

Check service health:

docker compose ps

Local Development vs Production

Local Development (pnpm dev)

  • Uses tsx for 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_modules with built workspace packages
  • Express API depends on external workspace packages
  • Nuxt uses self-contained .output directory (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.

Troubleshooting

Issue: Build fails with "permission denied"

# Use BuildKit
export DOCKER_BUILDKIT=1
docker-compose build

Issue: Port already in use

# Change ports in .env file
API_PORT=3012
NUXT_PORT=3011

Issue: Services can't connect to database

# Use Docker service name for DATABASE_URL
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres

Project Structure

turbo-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

Environment Configuration

API Environment Variables

Copy .env.example to .env in the API directory:

cp apps/api/.env.example apps/api/.env

Configure as needed:

  • PORT - API server port (default: 3002)
  • NODE_ENV - Environment (development/production)
  • CORS_ORIGIN - CORS allowed origin

Adding New Features

New API Routes

  1. Create route file in apps/api/src/routes/
  2. Export router and import in apps/api/src/routes/index.ts

New UI Components

  1. Add component to packages/ui/
  2. Export from packages/ui/index.ts
  3. Use in any app: import { MyComponent } from 'ui'

Shared Types

Add TypeScript types to packages/ui/types/ for cross-package usage.

Remote Caching

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

CI/CD Setup

TypeScript Configuration Generation

Nuxt and Nitro apps generate their TypeScript configurations at prepare time:

  • apps/nuxt/.nuxt/tsconfig.json - Generated by nuxt prepare
  • apps/nitro/.nitro/tsconfig.json - Generated by nitro prepare
  • apps/docus/.nuxt/tsconfig.json - Generated by nuxt prepare --extends docus

Local Development:

  • These are generated automatically via postinstall scripts when you run pnpm install
  • No manual action required

CI/CD Pipelines:

  • If you skip postinstall scripts (common practice with --ignore-scripts), you must run turbo prepare before typechecking
  • This ensures all .nuxt and .nitro directories are generated with proper TypeScript configurations

Example GitHub Actions Workflow

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 build

Manual Prepare Command

If 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 prepare

Useful Links

Learn more about the power of Turborepo:

Troubleshooting

TypeScript Configuration Not Found

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 typecheck

This is especially common in CI/CD environments when using pnpm install --ignore-scripts. See the CI/CD Setup section for more details.

better-sqlite3 Bindings Error

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-sqlite3

This 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.

Security Features (pnpm 10)

This starter uses pnpm 10 with enhanced security features to protect against supply chain attacks.

Lifecycle Script Protection

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 binaries

If you need to allow additional packages to run lifecycle scripts, add them to this list.

Minimum Release Age

New package versions are delayed by 24 hours before installation to give the community time to identify malicious releases.

minimumReleaseAge: 1440 # 24 hours in minutes

This 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.

Package Manager Version Management

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.

Learn More