Developer Guide
CrowdVision can be run in two distinct modes: Docker Compose for everyday local development, and Kubernetes for production-like environments. Both modes are managed via just recipes from the monorepo root.
For active development with live code reloading, use:
just stack devWhat happens:
just stack env runs first to verify your \.env and generate any missing keys (see Setting Up the Environment for the full list of what it generates).--watch — saving a file triggers an instant hot-reload inside the running container.Run a lighter stack by excluding services you are not working on:
# Exclude by substring of folder name:
just stack dev exclude="simulator"
# Exclude multiple services (space-separated):
just stack dev exclude="simulator frontend"
# Exclude the frontend (useful when running it separately with Vite):
just stack dev exclude="frontend"Exclusion matches by folder name substring. "simulator" will skip aq-simulator and sensor-simulator. Use a more specific substring to target a single service.
| Interface | URL | Description |
|---|---|---|
| Vue Frontend | http://localhost:8080 | The main CrowdVision user interface. |
| API Gateway | http://localhost:80 | Caddy proxy — routes to all microservices. |
| Keycloak | http://localhost:8090 | Hosted login/registration UI, and its own admin console (generated admin credentials — see Setting Up the Environment. Not routed through Caddy. |
| Langfuse | http://localhost:3030 (or http://localhost/langfuse, which redirects there) | LLM trace UI for the agent. Login admin@crowd-vision.local / langfuse-admin. |
| Twin DB GUI | http://localhost/twin-db-gui/ | Mongo Express for the twin database. Dev mode only. |
| Sensor DB GUI | http://localhost/sensor-db-gui/ | Mongo Express for the sensor database. Dev mode only. |
| Notification DB GUI | http://localhost/notification-db-gui/ | Mongo Express for the notification database. Dev mode only. |
| Contracts DB GUI | http://localhost/contracts-db-gui/ | Mongo Express for the contracts database. Dev mode only. |
| Chat DB GUI | http://localhost/chat-db-gui/ | Mongo Express for the chat database. Dev mode only. |
| Grafana | http://localhost/grafana/ | Metrics dashboards. Dev mode only. |
| Prometheus | http://localhost/prometheus/ | Raw metrics scraper. Dev mode only. |
just stack startRuns the stack using the base compose configuration in detached mode (-d). No hot-reloading, no database GUIs. Same exclude= syntax works here too.
# Graceful shutdown — stops all containers and removes orphans:
just stack down
# Follow logs for all services:
just stack logs
# Follow logs for one service:
just stack logs twin-service
# Wipe all databases (destructive — you will need to re-provision):
just db clear
# Wipe only the sensor database:
just db clear-sensorjust db clear permanently deletes all persistent Docker volumes.
Login and registration are Keycloak’s hosted UI now — there is no seed script for an admin account. To bootstrap a fresh stack: register through the frontend’s normal sign-in flow (redirects to Keycloak), then create your own domain through the Domains page (POST /tenancy/domains); the creator automatically becomes that domain’s business_admin. The old scripts/ops/provision.js (which called auth-service’s /business/register) has been removed along with auth-service — enterprise self-registration has no current equivalent, see Identity & Tenancy Architecture.
See the Kubernetes Configuration page for the full reference. Quick summary:
just k8s createSee Kubernetes Configuration for exactly what this creates and why (single-server k3d, Traefik disabled — Istio’s Gateway API is the local ingress path instead).
# First-time full setup (create cluster → Gateway API + Istio → namespace → secrets → manifests):
just k8s setup
# Subsequent deploys — re-apply manifests:
just k8s applyjust k8s build # build all images locally
just k8s load # import into k3d — no GHCR needed
just k8s apply # apply manifests# After code changes — rebuild the image, then rolling restart:
docker build -t ghcr.io/nickghignatti/crowdvision-twin:latest -f backend/twin-service/Dockerfile .
just k8s load
just k8s restart twin-service
# Watch the rollout:
just k8s rollout twin-servicetwin-service, agent-service, claims-gateway, tenancy-service, registry-service, and provisioner all build with -f backend/<service>/Dockerfile . from the repository root, not \./backend/<service>. Reason differs by language but is the same shape both times: twin-service/agent-service read the shared Cedar authorization policy bundle at backend/auth-policy and the role-weight ladder at backend/auth-contracts/roles.json, both siblings of their own directory; the four Go control-plane services depend on sibling Go modules (auth-contracts, auth-middleware, auth-policy) via a local replace directive rather than a registry, which only resolves if those siblings are inside the build context. chat-service, contracts-service, and the rest build from their own directory as shown above for the general case. just k8s build already gets every one of the twelve images right — see just/k8s.just for the exact command per service.
# Pause the cluster (preserves all pods, volumes, and secrets):
just k8s stop
# Resume from exactly where you left off (~30 seconds):
just k8s start
# Verify everything came back up:
just k8s status
# Destroy the cluster completely (data is permanently lost):
just k8s deleteUse just k8s stop at the end of your working day. All pod state and persistent volume data survives the stop/start cycle — your databases are intact.
# All pods should show 1/1 Running:
just k8s status
# Check persistent volumes are Bound:
kubectl get pvc -n crowdvision
# Check the Gateway and its routes are accepted (there is no Ingress — Istio's
# Gateway API is the only k8s ingress path):
kubectl get gateway,httproute -n crowdvision
# Tail logs for a service:
just k8s logs twin-service
# Describe a pod for detailed events (useful for debugging):
kubectl describe pod <pod-name> -n crowdvision# Run all unit tests across all services (moon caches results — unchanged services are instant):
just test all
# Run only tests for services touched by your current branch:
just test affected
# Per-service unit tests:
just test chat
just test twin
just test sensor
just test notification
just test socket
just test frontend
just test agent
# Agent-only Python integration suite (separate from the backend integration tests below):
just test agent-integration
# Full backend integration tests (spins up a composed stack, then tears it down):
just test integrationjust test all and just lint all both delegate to moon, which caches task results by input hash. If nothing in a service has changed since the last run, the cached result is replayed in milliseconds. Use just test affected / just lint affected on a feature branch to only process the services your changes actually touch. There is no just test <service> recipe for contracts-service or the four Go control-plane services (claims-gateway, provisioner, registry-service, tenancy-service) — they’re still covered by just test all / just test affected (both run moon run :test, which includes every registered project regardless of language), just not individually by name.
just test eval [models="..."] [judge="..."] runs the agent’s golden-dataset evaluation against your local stack, auto-minting the JWTs it needs. This isn’t a pass/fail test — it scores model responses (optionally with an LLM judge) and is meant to be run manually when tuning prompts or comparing models, not as part of the regular test loop. See backend/agent-service/evals/run_evals.py.
Two recipes run a security audit and a lockfile-sync check across every project. Both delegate to moon (moon run :audit / :deps), so they are cached and accept --affected to scope to the projects changed on your branch. See The Toolchain: mise & moon for how these tasks are defined.
# Security audit — npm audit (--audit-level=high --omit=dev) + uv audit + cargo audit:
just setup audit
# Lockfile-sync check — npm ci + uv sync --locked + cargo check:
just setup deps-check
# Scope either to only the projects changed on your branch:
mise exec -- moon run :audit --affectedjust setup deps-check is the fastest way to catch a lockfile drift before pushing (the classic npm ci can only install packages when your package.json and package-lock.json are in sync). If it fails, regenerate the offending lockfile with just setup clean-install (see Setting Up the Environment → Lockfiles & Cross-Platform Regeneration). The Rust cargo audit leg is marked allow-failure: if cargo-audit isn’t installed it is reported but does not fail the run (cargo install cargo-audit to enable it).
\.moon/tasks/go.yml defines build, test, lint (go vet), and deps (go mod download) for the four Go control-plane services, but no audit task — so just setup audit silently skips them (moon only runs a task for projects that define it). There’s no govulncheck-equivalent leg yet for claims-gateway, tenancy-service, registry-service, or provisioner.
| Recipe | What it does |
|---|---|
just stack dev [exclude="..."] | Full stack with hot-reloading. Optionally exclude services. |
just stack start [exclude="..."] | Full stack in detached/production mode. |
just stack down | Graceful shutdown, removes orphan containers. |
just stack logs [service] | Follow logs. Omit service to tail all. |
just db clear | Drop all databases (destructive). |
just db clear-sensor | Drop only the sensor database. |
just test all | Run all unit tests (moon-cached; unchanged services are instant). |
just test affected | Run unit tests only for services changed on the current branch. |
just test <service> | Run unit tests for one service (chat, twin, sensor, notification, socket, frontend, agent — not contracts or the Go services, see the note above). |
just test agent-integration | Agent-only Python integration suite (real Postgres, no full compose stack). |
just test eval [models=...] [judge=...] | Run the agent’s golden-dataset evaluation against the local stack. |
just test integration | Run backend integration tests. |
just agent ingest | Re-ingest all documentation into the agent’s knowledge base — run after editing anything under documentation/. |
just lint all | Lint every service (moon-cached). |
just lint affected | Lint only services changed on the current branch. |
just lint fix | Auto-fix linting issues across every service. |
just setup audit | Security audit across all projects via moon run :audit (npm + uv + cargo). |
just setup deps-check | Lockfile-sync check across all projects via moon run :deps (npm ci + uv sync —locked + cargo check). |
just setup clean-install | Wipe all node_modules + lockfiles and regenerate them for Linux, then reinstall. |
just docs build | Compile both Quarkdown guides to landing-page/{user,dev} (mirrors CI). |
just docs preview [dir=...] | Quarkdown live-reloading server for one guide while editing. |
just docs serve | Build both guides, then live-serve the whole landing-page/ site at :8080. |
The Caddy proxy forwards to the service container but nothing is listening. Most common causes:
1. The dev script in package.json points to a non-existent file. Every Node.js/TypeScript service must reference src/index.ts via ts-node/esm, not a compiled \.js file. Check the service’s package.json:
# Correct pattern (all TypeScript services):
"dev": "nodemon --watch src --exec node --no-warnings=ExperimentalWarning --loader ts-node/esm src/index.ts"If it says src/index.js and that file does not exist, nodemon exits silently and the service never starts — manifesting as 502.
2. Environment variable is empty or wrong. Open the service logs: just stack logs <service-name>. Look for connection errors to MongoDB or Redis at startup.
3. Missing \.env variable. Run just stack env to fill any gaps. The script skips variables already present, so it is safe to re-run.
just stack dev exclude="..." is not excluding the serviceExclusion works by matching the folder name substring. Examples:
just stack dev exclude="simulator" # skips aq-simulator and sensor-simulator
just stack dev exclude="simulator frontend" # also skips the Vue frontendIf exclusion suddenly stops working after editing the Justfile: verify that dev still takes only ONE parameter (exclude=""). Adding a second parameter before exclude in the recipe definition breaks named-argument resolution in just.
kubectl fails to connect after just k8s startAfter a cluster restart the kubeconfig may point to a stale port. Fix it:
# Find the actual port the k3d load balancer is using:
docker ps --filter name=k3d-crowdvision-serverlb
# Look for the port mapped to 6443 in the PORTS column, e.g. 0.0.0.0:32774->6443/tcp
# Update the kubeconfig to use the correct port:
kubectl config set-cluster k3d-crowdvision --server=https://127.0.0.1:<PORT>just k8s secrets fails: “namespaces not found”Secrets are scoped to the crowdvision namespace — it must exist first. Create it manually:
kubectl apply -f k8s/namespace.yml
just k8s secretsOr use just k8s setup which handles the ordering automatically.
If pods are stuck in ImagePullBackOff, Kubernetes is trying to pull from GHCR instead of using the locally loaded image.
# Load images from your local Docker daemon into k3d:
just k8s build
just k8s loadAlso check that the deployment manifest sets imagePullPolicy: IfNotPresent, not Always — Always forces a remote pull even when the image exists locally.