Local development stack
The local dev stack is what you’ll iterate against. It’s completely self-contained — no external accounts, no provider API keys required to boot. Same compose file is consulted on every CI run.
Topology
Section titled “Topology”docker-compose.yml — services + profiles
infra (always-on) postgres :5433 → 5432 inside the container redis :6380 → 6379 mailpit :1026 SMTP, :8026 UI seaweedfs :9000 S3 API (scani-dev / scani-dev-secret)
init (one-shot, exit cleanly) env-sync — materialises per-app .env files from root .env deps — bun install on the shared workspace volume migrate — applies the application and queue schemas to postgres
services (profile: full) data-provider :8082 api :3011 worker frontend :5173 (Vite dev server)Bring it up
Section titled “Bring it up”-
Seed the root env file.
Terminal window cp .env.example .envThe defaults work as-is for local development.
-
Boot.
Terminal window bun installbun run dev:stackbun run dev:stackisscripts/sync-env.ts+docker compose --profile full up -d --build. First boot pulls images and builds the app containers. -
Open the SPA.
Terminal window open http://localhost:5173 -
Sign in.
Magic-link emails land in Mailpit. Click the link there.
Bring it down
Section titled “Bring it down”bun run dev:stack:down # remove containers; volumes preservedTo also wipe Postgres / SeaweedFS / Redis data:
bun run dev:stack:down -- --volumesRun apps on the host instead of in containers
Section titled “Run apps on the host instead of in containers”Faster HMR, easier debugger attach, host-side breakpoints — useful when iterating on the api or worker:
bun run dev:stack:infra # Postgres, Redis, SeaweedFS, Mailpit — nothing elsebun installbun dev # api + frontend/appbun dev:worker # separate terminalbun dev:data-provider # separate terminalThe api binds PORT from the root .env on this path — 3001,
not the 3011 in Default ports below. See the note
under that table.
bun run dev:stackPer-app env files
Section titled “Per-app env files”The root .env is the single source of truth. scripts/sync-env.ts
materialises per-app .env files from it on every bun run dev:stack
and on every env-sync container run.
The script:
- Reads root
.env. - For each app under
apps/*/, parses that app’s.env.exampleto discover which keys it cares about. - Writes per-app
.envfiltered to only those keys. - Skips writes when content is unchanged (prevents Vite dev-server restart loops).
Adding a new env var an app needs: append it to that app’s
.env.example and to root .env.example. The script picks it up
automatically.
Service-to-service URLs in compose
Section titled “Service-to-service URLs in compose”The compose file overrides URLs that need a container-network
hostname (postgres, redis, seaweedfs, mailpit, data-provider)
on the relevant service’s environment: block. The root .env
versions are for host-side bun dev; the compose overrides take
precedence inside containers.
Default ports
Section titled “Default ports”Host ports published by the containerised stack — the
bun run dev:stack path. A linked git worktree offsets every one of them
and prints what it chose; the numbers below are what a plain checkout
gets.
On the host-side path (bun dev, above) only the api moves: it binds
PORT from the root .env, which .env.example ships as 3001.
3011 is the host port compose publishes for the containerised api
(container port 8080), and nothing publishes it when the api runs on
the host. The data-provider (8082) and the SPA (5173) are the same
on both paths.
The database the stack’s apps use is not scani. bun run dev:stack
creates one per checkout and passes its name as SCANI_DEV_DB; the
compose default scani applies only to a stack started without it.
Common dev tasks
Section titled “Common dev tasks”Each of these drives compose directly, so export this checkout’s identity
first — otherwise they address a project bun run dev:stack never
created, and say nothing about it:
export $(bun scripts/dev-stack.ts env | xargs)# Rebuild just one imagedocker compose --profile full up -d --build api
# Tail logs across servicesdocker compose --profile full logs -f api worker data-provider
# Run migrations on demand (when Drizzle generates new ones)docker compose --profile migrate run --rm migrate
# Open a psql shell on this checkout's databasedocker compose exec postgres psql -U scani "$SCANI_DEV_DB"
# Open a redis-clidocker compose exec redis redis-cli