Docs
Local development
Run the whole stack on your machine to customise or evaluate it.
You can run the whole ClientOS stack on your own machine to evaluate it, customize it or test changes before you deploy them. Local development uses the same code as production, with two local stand-ins: Mailpit catches every email instead of Resend, and MinIO stores files instead of R2.
Prerequisites#
| Tool | Version | Notes |
|---|---|---|
| Node.js | 20.12 or later | The production images use Node.js 24. |
| pnpm | 11 | The repository pins the exact version. Enable it with Corepack, which ships with Node.js. npm and Yarn can't install this workspace. |
| Docker | Docker Desktop or Docker Engine with Compose | Runs PostgreSQL, Redis, Mailpit and MinIO. |
| Git | Any recent version |
Enable pnpm through Corepack once:
corepack enable pnpm
Set up the project#
1. Install dependencies#
From the repository root:
pnpm install
2. Create the API's environment file#
The API and the database scripts read apps/api/.env. Copy the example, which already matches the local Docker services:
cp apps/api/.env.example apps/api/.env
Then open apps/api/.env and set SESSION_SECRET. It has no default, and the API won't start without it:
openssl rand -base64 32
Leave the mail and storage variables as they are. They point at Mailpit and MinIO. (pnpm dev copies the example for you if the file is missing, but you still need to set SESSION_SECRET.)
3. Start the local services#
pnpm db:up
This runs docker compose up -d with the repository's root docker-compose.yml, which is for local development only. It starts:
| Service | What it's for | Address |
|---|---|---|
| PostgreSQL 18 | The database (user, password and database are all clientos) | localhost:5432 |
| Redis 7 | Presence | localhost:6379 |
| Mailpit | Catches every email | SMTP on localhost:1025, inbox at http://localhost:8025 |
| MinIO | Stands in for R2 | S3 API on localhost:9000, console at http://localhost:9001 |
A one-off minio-init container also creates the clientos-media bucket.
4. Run the migrations#
pnpm db:migrate
This builds the shared packages, applies the migrations as the database owner (DATABASE_MIGRATION_URL), then creates or refreshes the API's own clientos_app login from the user and password in DATABASE_URL. It's the same path a production deploy runs.
5. Seed the defaults#
pnpm db:seed
The seed creates your workspace record, order statuses, expense categories, staff roles and a starter price book. It's safe to run again. Like production, it creates no staff accounts.
6. Optional: load demo data#
pnpm db:seed:demo
This fills the database with sample clients, orders, invoices and staff so every screen has something to show. To take the demo records back out:
pnpm db:seed:demo --clear
Only use demo data for looking around. Clearing it leaves the demo staff members on your team, so on a database you plan to keep, skip the demo and start from
/setup, or start again from a fresh database.
7. Start everything#
pnpm dev
pnpm dev starts the local Docker services if Docker is running, then the API, the portal and the admin, with each one's log prefixed in its own color. If something else is using one of its ports, it frees the port first. Wait for the line saying the API is answering before you open the apps.
Ports and URLs#
| App | URL |
|---|---|
| Portal | http://localhost:4101 |
| Admin | http://localhost:4102 |
| API | http://localhost:4200 (health check at http://localhost:4200/v1/health) |
| Mailpit inbox | http://localhost:8025 |
| MinIO console | http://localhost:9001 (user clientos, password clientos) |
If a port is already taken on your machine, you can move the Docker services by creating a .env file in the repository root with any of POSTGRES_PORT, REDIS_PORT, MAILPIT_SMTP_PORT, MAILPIT_UI_PORT, MINIO_PORT or MINIO_CONSOLE_PORT. If you move PostgreSQL, update the port in both database URLs in apps/api/.env to match.
Create a local owner#
Open http://localhost:4102. On a database with no one signed in yet, you're sent to the /setup installer. Locally it doesn't ask for a setup key, so just fill in the steps described in First run.
Or create the owner from the command line, once pnpm dev has built the API:
pnpm --filter @clientos/api bootstrap:owner --email [email protected] --name "Your Name"
It prints a one-time password. Like the installer, it refuses once any staff member has signed in.
Email and files locally#
You don't need Resend or R2 on your machine.
- Email. Outside production, if you haven't connected Resend in Settings, the API sends every email to Mailpit over SMTP. Open
http://localhost:8025to read sign-in codes, invitations and notifications. Nothing reaches a real inbox. - Files. If you haven't saved a bucket in Settings → Storage, uploads go to the local MinIO bucket
clientos-media. Logos are served through the API, because MinIO runs over plain HTTP.
These fallbacks come from the development-only variables in apps/api/.env. The API refuses them in production. See Configuration.
Everyday commands#
| Command | What it does |
|---|---|
pnpm dev | Starts the Docker services, the API, the portal and the admin |
pnpm dev:ui | Starts only the portal and admin |
pnpm dev admin api | Starts only the apps you name (customer, admin, api) |
pnpm dev --no-db | Starts the apps without touching Docker |
pnpm stop | Stops any dev server still listening on the app ports, for example after you closed a terminal instead of pressing Ctrl+C. The Docker services keep running. |
pnpm db:up / pnpm db:down | Starts or stops the local Docker services, keeping their data |
pnpm db:migrate | Applies migrations and refreshes the API's database login |
pnpm db:seed | Seeds the defaults |
pnpm db:seed:demo | Loads demo data (--clear removes it) |
pnpm db:studio | Opens Drizzle Studio to browse the database |
pnpm build | Builds every app and package |
pnpm check | Builds the shared packages, then runs lint, type checks and unit tests across the workspace |
pnpm test:e2e | Runs the Playwright browser tests. It starts the apps it needs by itself. |
To wipe your local data and start over:
docker compose down -v
pnpm db:up
pnpm db:migrate
pnpm db:seed
Before your first
pnpm test:e2e, Playwright may ask you to install its browsers. Runpnpm exec playwright installif it does.
Project layout#
ClientOS is a pnpm workspace.
| Path | What's in it |
|---|---|
apps/customer | The client portal (Next.js), including the public service pages, forms, signup and pay links. Deployed as the portal image. |
apps/admin | The agency admin (Next.js), including the /setup installer. |
apps/api | The API (NestJS on Fastify). It owns the database, permissions, payments, email, webhooks and background jobs. |
packages/db | The database schema (Drizzle), migrations, the seed and the migration and role commands. Only the API imports it. |
packages/domain | Framework-free business logic shared by the API and the apps: money, pricing, statuses, permissions and the catalog. |
packages/shared | The design system, app shell and shared UI used by the portal and admin. |
deploy/ | The production Dockerfile, Compose file and .env.example. See Installation. |
scripts/ | The scripts behind pnpm dev, pnpm stop and the demo seed. |
docker-compose.yml | Local development services only. Never use it in production. |
When you're ready to ship your changes, follow Installation for a first deploy, or Updates and backups for an existing one.