This guide documents the local development workflow for contributors working on the Statica codebase.
It covers:
- first-time setup
- day-to-day development in the main checkout
- isolated worktree development
- the shared PostgreSQL model
- testing and verification
- full-stack isolated testing (backend + frontend + daemon from source)
- troubleshooting and destructive reset options
Local development uses one shared PostgreSQL container and one database per checkout.
- the main checkout usually uses
.envandPOSTGRES_DB=statica - each Git worktree uses its own
.env.worktree - every checkout connects to the same PostgreSQL host:
localhost:5432 - isolation happens at the database level, not by starting a separate Docker Compose project
- backend and frontend ports are still unique per worktree
This keeps Docker simple while still isolating schema and data.
- Node.js
v20+ pnpmv10.28+- Go
v1.26+ - Docker
- The main checkout should use
.env. - A worktree should use
.env.worktree. - Do not copy
.envinto a worktree directory.
Why:
- the current command flow prefers
.envover.env.worktree - if a worktree contains
.env, it can accidentally point back to the main database
Create .env once:
cp .env.example .envBy default, .env points to:
POSTGRES_DB=statica
POSTGRES_PORT=5432
DATABASE_URL=postgres://statica:statica@localhost:5432/statica?sslmode=disable
PORT=8080
FRONTEND_PORT=3000Generate .env.worktree from inside the worktree:
make worktree-envThat generates values like:
POSTGRES_DB=statica_my_feature_702
POSTGRES_PORT=5432
PORT=18782
FRONTEND_PORT=13702
DATABASE_URL=postgres://statica:statica@localhost:5432/statica_my_feature_702?sslmode=disableNotes:
POSTGRES_DBis unique per worktreePOSTGRES_PORTstays fixed at5432- backend and frontend ports are derived from the worktree path hash
make worktree-envrefuses to overwrite an existing.env.worktree
To regenerate a worktree env file:
FORCE=1 make worktree-envFrom any checkout (main or worktree):
make devThis single command:
- auto-detects whether you're in a main checkout or a worktree
- creates the appropriate env file (
.envor.env.worktree) if it doesn't exist - checks that prerequisites (Node.js, pnpm, Go, Docker) are installed
- installs JavaScript dependencies
- ensures the shared PostgreSQL container is running
- creates the application database if it does not exist
- runs all migrations
- starts both backend and frontend
If you prefer separate control over setup and startup:
cp .env.example .env
make setup-main
make start-mainStop:
make stop-mainmake worktree-env
make setup-worktree
make start-worktreeStop:
make stop-worktreeUse the main checkout when you want a stable local environment for main.
make start-main
make stop-main
make check-mainUse a worktree when you want isolated data and separate app ports.
git worktree add ../statica-feature -b feat/my-change main
cd ../statica-feature
make devAfter that, day-to-day commands are:
make dev # start (re-runs setup if needed, idempotent)
make stop-worktree # stop
make check-worktree # verifyThis is a first-class workflow.
Example:
- main checkout
- database:
statica - backend:
8080 - frontend:
3000
- database:
- worktree checkout
- database:
statica_my_feature_702 - backend: generated worktree port such as
18782 - frontend: generated worktree port such as
13702
- database:
Both checkouts use:
- the same PostgreSQL container
- the same PostgreSQL port:
5432
But they do not share application data, because each uses a different database.
Start the shared PostgreSQL container:
make db-upStop the shared PostgreSQL container:
make db-downImportant:
make db-downstops the container but keeps the Docker volume- your local databases are preserved
Main checkout:
make setup-main
make start-main
make stop-main
make check-mainWorktree:
make worktree-env
make setup-worktree
make start-worktree
make stop-worktree
make check-worktreeGeneric targets for the current checkout:
make setup
make start
make stop
make check
make dev
make test
make migrate-up
make migrate-downThese generic targets require a valid env file in the current directory.
Database creation is automatic.
The following commands all ensure the target database exists before they continue:
make setupmake startmake devmake testmake migrate-upmake migrate-downmake check
That logic lives in scripts/ensure-postgres.sh.
Run all local checks:
make check-mainOr from a worktree:
make check-worktreeThis runs:
- TypeScript typecheck
- TypeScript unit tests
- Go tests
- Playwright E2E tests
Notes:
- Go tests create their own fixture data
- E2E tests create their own workspace and issue fixtures
- the check flow starts backend/frontend only if they are not already running
Run the local daemon:
make daemonThe daemon authenticates using the CLI's stored token (statica login).
It registers runtimes for all watched workspaces from the CLI config.
This section covers running the complete stack (backend, frontend, daemon) from source in a fully isolated environment. Useful for testing end-to-end changes that span multiple components, or for automated CI/AI workflows that need zero human intervention.
make daemon uses the system-installed CLI's stored token and connects to
whatever server is configured in ~/.statica/config.json. That's fine for
day-to-day development against a shared server, but for fully isolated testing
you need:
- a local backend and frontend (from source)
- a local daemon (from source) with its own profile
- automated authentication (no browser login)
- no interference with your production CLI config
Each worktree must use a unique daemon profile to avoid collisions when multiple features run in parallel.
The profile name is derived from the worktree directory using the same
slug + hash pattern as scripts/init-worktree-env.sh:
WORKTREE_DIR="$(basename "$PWD")"
SLUG="$(printf '%s' "$WORKTREE_DIR" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/_/g; s/__*/_/g; s/^_//; s/_$//')"
HASH="$(printf '%s' "$PWD" | cksum | awk '{print $1}')"
OFFSET=$((HASH % 1000))
PROFILE="dev-${SLUG}-${OFFSET}"Example: worktree at ../statica-feat-auth produces profile
dev-statica_feat_auth-347, matching that worktree's port and database
allocation.
Run all steps from the worktree root (where the Makefile is).
make devWait for the backend to be healthy:
PORT=$(grep '^PORT=' .env.worktree 2>/dev/null || grep '^PORT=' .env | head -1 | cut -d= -f2)
PORT=${PORT:-8080}
SERVER="http://localhost:${PORT}"
for i in $(seq 1 30); do
curl -sf "$SERVER/health" > /dev/null 2>&1 && break
sleep 2
doneIn non-production environments the verification code is fixed at 888888:
curl -s -X POST "$SERVER/auth/send-code" \
-H "Content-Type: application/json" \
-d '{"email": "dev@localhost"}'
JWT=$(curl -s -X POST "$SERVER/auth/verify-code" \
-H "Content-Type: application/json" \
-d '{"email": "dev@localhost", "code": "888888"}' | jq -r '.token')
PAT=$(curl -s -X POST "$SERVER/api/tokens" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{"name": "auto-dev", "expires_in_days": 365}' | jq -r '.token')WS=$(curl -s -X POST "$SERVER/api/workspaces" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{"name": "Dev", "slug": "dev"}' | jq -r '.id')# Compute profile (see Dynamic Profile Naming above)
WORKTREE_DIR="$(basename "$PWD")"
SLUG="$(printf '%s' "$WORKTREE_DIR" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/_/g; s/__*/_/g; s/^_//; s/_$//')"
HASH="$(printf '%s' "$PWD" | cksum | awk '{print $1}')"
OFFSET=$((HASH % 1000))
PROFILE="dev-${SLUG}-${OFFSET}"
FRONTEND_PORT=$(grep '^FRONTEND_PORT=' .env.worktree 2>/dev/null || grep '^FRONTEND_PORT=' .env | head -1 | cut -d= -f2)
FRONTEND_PORT=${FRONTEND_PORT:-3000}
CONFIG_DIR="$HOME/.statica/profiles/$PROFILE"
mkdir -p "$CONFIG_DIR"
cat > "$CONFIG_DIR/config.json" << EOF
{
"server_url": "$SERVER",
"app_url": "http://localhost:${FRONTEND_PORT}",
"token": "$PAT",
"workspace_id": "$WS",
"watched_workspaces": [{"id": "$WS", "name": "Dev"}]
}
EOFmake cli ARGS="daemon start --profile $PROFILE"The daemon runs from the current worktree's Go source, connecting to the
local backend. Agent-executed statica commands automatically use the same
binary (the daemon prepends its own directory to PATH).
# Compute profile (same formula)
PROFILE="dev-$(printf '%s' "$(basename "$PWD")" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/_/g; s/__*/_/g; s/^_//; s/_$//')-$(( $(printf '%s' "$PWD" | cksum | awk '{print $1}') % 1000 ))"
# 1. Stop daemon
make cli ARGS="daemon stop --profile $PROFILE"
# 2. Stop backend + frontend
make stop # main checkout
make stop-worktree # worktree checkout
# 3. (Optional) Stop shared PostgreSQL
make db-down
# 4. (Optional) Clean build artifacts
make clean
# 5. (Optional) Remove profile config
rm -rf "$HOME/.statica/profiles/$PROFILE"To test the Electron desktop app against a local backend:
# After backend is running (make dev)
pnpm dev:desktopThis automatically:
- Compiles the
staticaCLI fromserver/cmd/staticaintoapps/desktop/resources/bin/statica - Creates an isolated profile named
desktop-localhost-<PORT> - Starts and manages its own daemon instance
- Connects to the local backend
Login in the Desktop UI with dev@localhost and code 888888.
If the backend runs on a non-default port (worktree), create
apps/desktop/.env.development.local:
VITE_API_URL=http://localhost:<backend-port>
VITE_WS_URL=ws://localhost:<backend-port>/wsNothing in this flow touches the system-installed statica or the default
~/.statica/config.json:
| Resource | System / Production | Local Dev (per-worktree) |
|---|---|---|
| Config | ~/.statica/config.json |
~/.statica/profiles/dev-<slug>-<hash>/config.json |
| Daemon PID | ~/.statica/daemon.pid |
~/.statica/profiles/dev-<slug>-<hash>/daemon.pid |
| Health port | 19514 |
19514 + 1 + (name_hash % 1000) |
| Workspaces dir | ~/statica_workspaces/ |
~/statica_workspaces_dev-<slug>-<hash>/ |
| Database | remote / production | local Docker: statica_<slug>_<hash> |
| Desktop profile | desktop-api.statica.ai |
desktop-localhost-<port> |
Multiple worktrees can run simultaneously without conflict.
If you see:
Missing env file: .env
or:
Missing env file: .env.worktree
then create the expected env file first.
Main checkout:
cp .env.example .envWorktree:
make worktree-envInspect the env file:
cat .env
cat .env.worktreeLook for:
POSTGRES_DBDATABASE_URLPORTFRONTEND_PORT
docker compose exec -T postgres psql -U statica -d postgres -At -c "select datname from pg_database order by datname;"Check whether the worktree contains .env.
It should not.
The safe worktree setup is:
make worktree-env
make setup-worktree
make start-worktreeThat is expected.
make stopmake stop-mainmake stop-worktree
only stop backend/frontend processes.
To stop the shared PostgreSQL container:
make db-downIf you want to stop PostgreSQL and keep your local databases:
make db-downIf you want to wipe all local PostgreSQL data for this repo:
docker compose down -vWarning:
- this deletes the shared Docker volume
- this deletes the main database and every worktree database in that volume
- after that you must run
make setup-mainormake setup-worktreeagain
make devgit worktree add ../statica-feature -b feat/my-change main
cd ../statica-feature
make devcd ../statica-feature
make start-worktreeMain checkout:
make check-mainWorktree:
make check-worktree