Docs/Hearth/Local dev

Local dev

hearth up starts the local Docker Compose stack defined in hearth/compose.yaml. Everything talks over a single shared Docker network called katforge, fronted by Traefik with TLS.

Services

ServiceImageRole
traefiktraefik:v3.1Ingress, TLS, host routing
dbpostgres:16-alpinePostgres 16, dev data only
apibuilt from ../api.katforge.comSymfony API
cronbuilt from ../api.katforge.comBackground commands (Stumper challenge gen, lobby cleanup)
pgadmindpage/pgadmin4:latestPre-wired pgAdmin pointing at db
mercuredunglas/mercure:v0.16SSE hub for realtime fan-out
depsoven/bun:1One-shot bun install for the workspace
katforgenode:22Nuxt dev server
stumperoven/bun:1Vite dev server
lextrisoven/bun:1Vite dev server
geargoblinsoven/bun:1Vite dev server

The frontend services depend on deps having completed successfully — that runs bun install for the entire workspace once per hearth up so subsequent frontends start instantly.

flowchart LR
   deps["deps<br/>bun install"]
   db["db · mercure · traefik"]
   api["api · cron"]
   front["katforge · stumper · lextris · geargoblins"]
   db --> api --> front
   deps --> front
   classDef infra fill:#0f172a,stroke:#334155,color:#cbd5e1
   classDef hot fill:#0f1f0f,stroke:#16a34a,color:#86efac
   class db,api infra
   class deps,front hot

The katforge Docker network

All services join an external network called katforge. App stacks outside this repo can attach to it to reach the API and database without exposing host ports.

YAML
networks:
   katforge:
      external: true
      name: katforge

services:
   ui:
      networks:
         - katforge

hearth up creates the network if it doesn't exist; hearth down does not delete it (other compose stacks might still need it).

Env injection

Every env var on every service is marked required in compose.yaml:

YAML
environment:
   APP_SECRET: ${APP_SECRET:?required}
   DATABASE_URL: ${DATABASE_URL:?required}
   OAUTH_DISCORD_ID: ${OAUTH_DISCORD_ID:?required}
   # ...

hearth up decrypts hearth/env/<stage>/*.enc.yaml with sops, merges with the plaintext *.yaml siblings, and exports every key into the process environment before invoking compose. Missing values surface as a fast failure rather than silently defaulting to empty strings.

The :? form fails if the var is empty or undefined. The ? form (no colon) only fails if the var is undefined — empty is fine. The compose file uses both depending on whether the empty case is meaningful (e.g. an optional OAuth provider).

Persistent volumes

VolumeHolds
db_dataPostgres data
mercure_dataMercure bolt transport (SSE history)
pgadmin_datapgAdmin config
bun_modulesWorkspace node_modules for the frontends
katforge_modulesNuxt's node_modules

Test accounts

All accounts use password abc123. The dev stage seeds these on first boot.

EmailUsernamePurpose
admin@test.localadminHas characters, populated
user@test.localuserEmpty state, new player
tmarcus@katforge.comtmarcusDev account

Direct port mappings

In addition to Traefik routing, each service publishes a host port for direct access (useful when bypassing the proxy):

ServiceInternalExternal
APIapi:8000localhost:8100
Postgresdb:5432localhost:5433
Mercuremercure:80localhost:8181
pgAdminpgadmin:80localhost:5050

Resetting state

shell
hearth down
docker volume rm katforge-hearth_db_data       # nuke the DB
docker volume rm katforge-hearth_mercure_data  # reset SSE history
hearth up

Resetting mercure_data is a useful trick when SSE replay history gets weird; subscribers reconnect fresh.