Local Dev Setup
Get your development environment running from scratch.
This guide walks you through setting up a local Toast development environment. For the short version, see the CONTRIBUTING.md checklist.
Prerequisites
- Node.js 24+ — check with
node --version - pnpm — installed via corepack (
corepack enable) - Docker — required for PostgreSQL and VersityGW (object storage)
- Git — for cloning and hooks
Quick Start
git clone https://github.com/TryGhost/Toast.git
cd Toast
pnpm install
pnpm dxpnpm dx is the single command that handles everything: starts Docker containers, runs database migrations, seeds data, and launches all dev servers.
What pnpm dx Does
On first run, pnpm dx detects it's a fresh checkout and runs the bootstrap sequence:
- Port detection — checks if default ports (5432, 9000, 3000, 5173) are available
- Environment setup — creates
.envfrom.env.example, prompting for custom ports if needed - Docker Compose — starts PostgreSQL 16 and VersityGW (S3-compatible object storage)
- Database migrations — applies all Drizzle ORM migrations
- Database seeding — creates a default site and admin user
- Dev servers — starts the API server (port 3000) and admin panel (port 5173) via Turborepo
On subsequent runs, it skips the bootstrap and goes straight to starting Docker and dev servers.
Non-Interactive Mode
For CI or scripting, skip all prompts:
DX_AUTO=1 pnpm dxOr manually create .env before running:
cp .env.example .env
# Edit ports if needed
pnpm dxTipTap Pro Extensions (optional)
pnpm install needs no token or registry setup — the repo has no private dependencies. The editor's Pro extensions (AI, comments, and the collaboration provider) are a paid, optional add-on and are not installed by default. The editor degrades gracefully without them via apps/admin/src/lib/tiptap-pro-adapter.ts.
To enable them you need a paid TipTap Cloud subscription. Set the registry token and run the opt-in installer:
export TIPTAP_PRO_TOKEN=<token-from-tiptap-cloud>
./scripts/enable-tiptap-pro.shThis installs the @tiptap-pro/* packages into your checkout as a local overlay. It does not add them to any package.json or the committed lockfile, so the repo stays reproducible and token-free — do not commit the resulting apps/admin/package.json / pnpm-lock.yaml changes. The script writes registry auth to your user-level ~/.npmrc (pnpm 10 does not expand env vars in the project .npmrc).
TipTap Runtime Features
TipTap has a two-layer model:
TIPTAP_PRO_TOKEN— private-registry auth used only by./scripts/enable-tiptap-pro.shto install the optional Pro extensions; not needed for a normal install or build- Runtime capabilities — controlled by env pairs:
- Collaboration:
VITE_TIPTAP_COLLAB_APP_ID+TIPTAP_COLLAB_SECRET - AI:
VITE_TIPTAP_AI_APP_ID+TIPTAP_AI_SECRET
- Collaboration:
The admin panel checks /api/capabilities at runtime to gate features — it never reads env vars directly.
Verify Setup
After pnpm dx completes, verify everything is running:
# API health check
curl http://localhost:3000/healthz # {"status":"ok"}
curl http://localhost:3000/healthz/ready # {"status":"ok","database":"connected"}The admin panel runs at http://localhost:5173. Default login:
- Email:
admin@ghost.org - Password:
admin
Docker Services
docker-compose.dev.yml manages two services:
| Service | Image | Default Port | Purpose |
|---|---|---|---|
| db | postgres:16-alpine | 5432 | Primary database |
| storage | ghcr.io/versity/versitygw | 9000 / 9001 / 9002 | S3-compatible file storage |
A storage-setup initializer creates toast-uploads and permits anonymous object downloads only. pnpm dx waits for it to succeed before starting the app. The console at http://localhost:9001 uses minioadmin / minioadmin, retained for compatibility with existing local settings. Its Admin API uses port 9002. All storage ports bind to localhost.
Set S3_PORT, S3_CONSOLE_PORT and S3_ADMIN_PORT to customize these ports, and keep S3_ENDPOINT / S3_PUBLIC_URL aligned with the API port. MINIO_PORT, MINIO_CONSOLE_PORT and the worktree --minio argument remain supported as fallbacks; new worktrees use provider-neutral names. pnpm dx derives missing console/admin ports for existing worktrees; pnpm worktree:ports:check detects conflicts.
Existing MinIO data
VersityGW starts with new storage_data and storage_iam volumes. The old minio_data volume is left untouched; it cannot be mounted directly into VersityGW. If you need its objects, keep an existing working MinIO container/image, start it separately on different ports, and copy objects through the S3 API into VersityGW. Verify the copied objects before removing any old container or volume. If the old image is unavailable, preserve its volume until a recovery path is available.
Stop the old MinIO container before starting the replacement on the same ports. Do not run docker compose down -v as a migration step: it also deletes database volumes. A fresh storage volume does not remove database records, so existing media references require an object copy or re-upload.
Run pnpm test:storage to verify the real storage contract using isolated disposable containers and volumes. CI runs this alongside database integration tests.
Port Conflicts
If port 5432 is already in use (e.g., a system PostgreSQL), you have two options:
- Interactive — run
pnpm dxand choose custom ports when prompted - Manual — edit
.envto setDB_PORT=5433and updateDATABASE_URLaccordingly
Git Worktrees
When working on multiple branches simultaneously, use worktrees instead of stashing:
# Create a worktree with auto-allocated ports
pnpm worktree:create my-feature-branch
# Check which ports your worktree uses
pnpm worktree:context --json
# List all worktrees and their ports
pnpm worktree:list --json
# Detect and fix port collisions
pnpm worktree:ports:check
pnpm worktree:ports:fixEach worktree gets its own .env with unique ports so multiple branches can run dev servers simultaneously without conflicts.
Important: In a worktree, never assume default ports from docs. Always run pnpm worktree:context --json first.
Common Commands
| Task | Command |
|---|---|
| Start everything | pnpm dx |
| Run tests | pnpm test |
| Run a single API test | pnpm --filter @toast/api test src/app.docs.test.ts |
| Integration tests | pnpm test:integration |
| Type check | pnpm typecheck |
| Lint + fix | pnpm lint:fix |
| Format + fix | pnpm format:fix |
| Full quality check | pnpm check |
| Strict check (no fix) | pnpm check:strict |
| Generate DB migration | td db generate <name> |
| Apply migrations | td db migrate |
| Reset database | td db reset |
| Browse DB (Drizzle Kit) | td db studio |
Editor Setup
Install the Oxc VS Code extension for real-time lint and format feedback.
See Linting & Formatting for the full rule set.
Troubleshooting
pnpm install fails with 404 errors for @tiptap-pro/*
A plain pnpm install should never hit this — the repo has no private dependencies. If you see it, you likely have a leftover local TipTap Pro overlay (from ./scripts/enable-tiptap-pro.sh) with a missing or expired TIPTAP_PRO_TOKEN. Either set a valid token or remove the overlay (git checkout apps/admin/package.json pnpm-lock.yaml && pnpm install). See TipTap Pro Extensions above.
Docker containers won't start
Check that Docker Desktop is running and no other containers are using the same ports:
docker ps # List running containers
docker compose -f docker-compose.dev.yml down # Stop Toast containers
docker compose -f docker-compose.dev.yml up -d # RestartDatabase connection errors
Verify the database is healthy:
docker compose -f docker-compose.dev.yml ps # Check container status
td db migrate # Re-run migrations
td db reset # Nuclear option: reset and re-seedPort already in use
If a dev server fails to start because a port is taken:
lsof -i :3000 # Find what's using the port
pnpm worktree:ports:check # Check for worktree port collisions