Development Guide

The CLAUDE.md at the repo root has the canonical "run the dev stack" instructions and architectural facts the next session must know — start there. This file expands on the developer workflow and conventions.


Prerequisites

Tool Why Notes

Rust stable (≥ 1.83)

Backend

CI uses rust:1.83 Docker image

Rust nightly-2026-04-25

Frontend

Pinned in frontend/rust-toolchain.toml; required for some rust-ui components that use bind:value (unstable Leptos feature) and let-chains (edition 2024)

wasm32-unknown-unknown target

Frontend

Auto-installed by toolchain file

trunk

Frontend dev server / bundler

cargo install trunk (CI does this; locally do it once)

wasm-bindgen-cli

Frontend

cargo install wasm-bindgen-cli

cargo-sqlx (a.k.a. sqlx-cli)

Backend

Only needed when adding/modifying SQL queries — cargo install sqlx-cli --no-default-features --features postgres,rustls

docker

Postgres + (optionally) Compose stack

python3

Mock JWKS server

Stdlib only

The pre-commit hook config in .pre-commit-config.yaml runs split clippy hooks per crate so backend (stable) and frontend (nightly+wasm32) check correctly. Run pre-commit install once after cloning.


First-time setup

# 1. Postgres on :5433 (matches DEV_AUTH wiring in scripts)
docker run -d --name shiftit-pg \
  -e POSTGRES_USER=shiftit -e POSTGRES_PASSWORD=shiftit -e POSTGRES_DB=shiftit \
  -p 5433:5432 postgres:16-alpine

# 2. Apply demo seed (idempotent — see scripts/seed-demo.sql)
docker exec -i shiftit-pg psql -U shiftit -d shiftit < scripts/seed-demo.sql

Daily dev loop

Three components, three terminals (or & in one):

# A. Mock JWKS so JwksCache::new succeeds without a real Zitadel
python3 scripts/mock-jwks.py 9999

# B. Backend with DEV_AUTH bound to one of the seeded users (admin|employee|customer)
./scripts/run-backend.sh admin

# C. Frontend dev server with hot-reload, proxies /api/* to :3010
cd frontend && trunk serve

Endpoints:

To enter the auth-gated UI, paste in the browser console once:

localStorage.setItem('shiftit_token', JSON.stringify('dev')); location.reload();

The frontend stores tokens JSON-encoded via gloo-storage; any non-empty string works in dev because the backend ignores tokens entirely when DEV_AUTH_* env vars are set.

To switch user role mid-session, kill the backend and re-run ./scripts/run-backend.sh employee (or customer).


Build & test

Action Command

Format check

cargo fmt --all -- --check

Backend clippy

cargo clippy -p shiftit-backend --all-targets --all-features -- -D warnings

Frontend clippy

cd frontend && cargo clippy -p shiftit-frontend --target wasm32-unknown-unknown --all-targets -- -D warnings

Backend tests

cargo test --manifest-path backend/Cargo.toml -- --nocapture (requires DATABASE_URL; CI uses a sidecar postgres:16-alpine)

Frontend prod build

cd frontend && trunk build --release

Backend prod build

cargo build --release --manifest-path backend/Cargo.toml

sqlx offline cache rebuild

cargo sqlx prepare --workspace -- --tests (commit .sqlx/ after)

License-tool tests

cargo test --manifest-path deploy/license/Cargo.toml -- --nocapture (separate workspace)


Conventions

  • Branch model: main only; no PRs locally — every commit lands on main directly. Cargo.lock is committed.

  • Commit style: conventional-ish prefixes (feat:, feat(frontend):, fix:, docs:, chore:). Run git log --oneline -20 for examples.

  • Migrations: new migrations follow Plan 02 conventions — explicit FOR ALL + WITH CHECK on every RLS policy, table-distinguished policy names (<table>_isolation), and explicit GRANTs to shiftit_app. After adding/modifying SQL, run cargo sqlx prepare.

  • Migration discipline: migrations 0001 and 0005 were edited in-place after first apply. If you see migration X was previously applied but has been modified on cargo run, drop+recreate the dev DB:

    docker exec shiftit-pg psql -U shiftit -d postgres -c \
      "DROP DATABASE IF EXISTS shiftit; DROP ROLE IF EXISTS shiftit_app; CREATE DATABASE shiftit;"
    docker exec -i shiftit-pg psql -U shiftit -d shiftit < scripts/seed-demo.sql
  • Adding routes: annotate the handler with #[utoipa::path], register the path in backend/src/main.rs::create_router, AND add it to the paths(...) list in backend/src/openapi.rs::ApiDoc. The third step is easy to forget — the openapi_doc_includes_expected_path_count test catches catastrophic regressions but not single missing entries.

  • Tenant-scoped queries: always go through ctx.rls_transaction(&pool). Never use the pool directly from a handler that’s tenant-scoped; the SET ROLE shiftit_app is on the connection, but app.tenant_id is set by rls_transaction only.

  • Frontend fetch: LocalResource::new(fetcher) for reads, Action::new_local(...) for writes. gloo-net futures are !Send on wasm32 so the local variants are mandatory.

  • No JS in frontend: WASM-only by design. The rust-ui Dialog/Popover/DropdownMenu components emit <script> tags referring to window.ScrollLock; we haven’t pulled those shims yet. Don’t use those components until follow-up #8 is resolved.


Troubleshooting

Symptom Cause / fix

migration X was previously applied but has been modified

See migration discipline above — drop+recreate.

Backend panics on startup with "missing public key"

Non-saas dev build needs SHIFTIT_PUBLIC_KEY env var or use --features saas. The default scripts/run-backend.sh uses --features saas to skip the license check.

trunk serve builds but UI is blank

Check browser console for gloo-net errors. Common: 401 from backend → token not set in localStorage.

cargo run hangs after migrations

JwksCache::new() is trying to reach ZITADEL_DOMAIN. Make sure scripts/mock-jwks.py 9999 is running.

cross-tenant data leakage in tests

TenantContext::rls_transaction not used, or connect_db() skipped (e.g. raw PgPool::connect in a test). The runtime pool’s after_connect hook is what enforces RLS.