Skip to content

Local Setup

  • Node.js >= 18
  • pnpm >= 9
  • Git

One command bootstraps everything — installs deps, runs migrations, seeds the database, starts all dev servers, and health-checks each service:

Terminal window
git clone <repo-url>
cd construction
pnpm dev:up

dev:up performs these steps automatically:

  1. Installs dependencies (pnpm install)
  2. Runs all D1 database migrations
  3. Seeds the dev database with a test tenant + admin account
  4. Starts API, Portal, and Intake Form dev servers
  5. Health-checks each service
  6. Seeds 50 mock leads via the intake webhook
  7. Prints a summary with URLs and test credentials

After the first run, a .dev-ready marker file is created. Subsequent pnpm dev:up calls skip the install/migrate/seed steps and go straight to starting servers.

Terminal window
pnpm dev:up # Start all services
pnpm dev:down # Stop all services (kills wrangler, vite, astro)
ScriptCommandPurpose
Start allpnpm dev:upFull bootstrap + start all 3 dev servers
Stop allpnpm dev:downKill all dev processes, verify ports are free
Re-initpnpm dev:initForce re-run migrations + seed (no server start)
Full resetpnpm dev:nukeWipe all local state (node_modules, DB, build artifacts)

After dev:nuke, run pnpm dev:up to rebuild from scratch.

AppDefault PortURL
API8201http://localhost:8201/api/v1/health
Portal8200http://localhost:8200/login
Intake Form8202http://localhost:8202/contractor
Docs8203http://localhost:8203

Ports are configurable via config/.tenant.dev.env:

DEV_PORT_PORTAL=8200
DEV_PORT_API=8201
DEV_PORT_INTAKE=8202
Terminal window
pnpm dev:api # API only (port 8201)
pnpm dev:portal # Portal only (port 8200)
# Intake: cd apps/lead-intake-form-pwa && pnpm dev
# Docs: cd apps/docs && pnpm dev
FieldValue
Emailsuadmin@dev.local
PasswordSet via DEFAULT_SUADMIN_PASSWORD env var

This account is an admin role with full access to all portal features.

The portal dev server proxies /api/v1/* requests to the API at http://localhost:8201. This is configured in apps/project-work-portal/vite.config.ts. In production, Cloudflare Pages Functions handle the proxy.

ContractorHub uses Cloudflare D1 (SQLite) with Drizzle ORM.

Terminal window
# Run migrations manually
cd apps/api && pnpm db:migrate
# Re-seed (idempotent — uses INSERT OR IGNORE)
cd apps/api && node scripts/seed-account.mjs all
# Full DB reset (nuke wrangler state + re-migrate + re-seed)
rm -rf apps/api/.wrangler && cd apps/api && pnpm db:migrate && node scripts/seed-account.mjs all

Migrations are in apps/api/migrations/ and run in alphabetical order.

Terminal window
# All unit + integration tests
pnpm test
# Specific packages
pnpm test:shared # packages/shared
cd apps/api && pnpm test # API (unit + integration)
cd apps/project-work-portal && pnpm test # Portal unit tests
# E2E tests (auto-starts API + Portal servers)
cd apps/project-work-portal && npx playwright test
# Single E2E spec
npx playwright test quote-builder
npx playwright test quote-share
# Type checking
cd apps/project-work-portal && pnpm tsc --noEmit

Tenant settings live in config/.tenant.dev.env (committed, for development) and config/.tenant.env (production, gitignored).

Key settings: company name, phone, email, office address, dev ports. See the Tenant Config reference for all available fields.

Port already in use: Run pnpm dev:down to kill stale processes, then retry pnpm dev:up.

Database errors after pulling new migrations: Delete .dev-ready to force re-initialization:

Terminal window
rm .dev-ready && pnpm dev:up

Full clean start:

Terminal window
pnpm dev:nuke && pnpm dev:up

Wrangler stale state: If D1 errors persist after migration changes:

Terminal window
rm -rf apps/api/.wrangler && pnpm dev:up