/

Developer Guide

Run the Application

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.


Docker Compose Mode

Development (Hot-Reloading)

For active development with live code reloading, use:

just stack dev

What happens:

  1. 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).
  2. A runtime compose file is generated that includes all service compose files.
  3. Docker Compose launches with --watch — saving a file triggers an instant hot-reload inside the running container.
  4. All services start: Caddy proxy, Redis, Keycloak, every service’s own database (MongoDB or Postgres, depending on the service), all backend microservices, and the Vue frontend.

Excluding Services

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"

Note

Exclusion matches by folder name substring. "simulator" will skip aq-simulator and sensor-simulator. Use a more specific substring to target a single service.

Accessing the Application (Docker mode)

InterfaceURLDescription
Vue Frontendhttp://localhost:8080The main CrowdVision user interface.
API Gatewayhttp://localhost:80Caddy proxy — routes to all microservices.
Keycloakhttp://localhost:8090Hosted login/registration UI, and its own admin console (generated admin credentials — see Setting Up the Environment. Not routed through Caddy.
Langfusehttp://localhost:3030 (or http://localhost/langfuse, which redirects there)LLM trace UI for the agent. Login admin@crowd-vision.local / langfuse-admin.
Twin DB GUIhttp://localhost/twin-db-gui/Mongo Express for the twin database. Dev mode only.
Sensor DB GUIhttp://localhost/sensor-db-gui/Mongo Express for the sensor database. Dev mode only.
Notification DB GUIhttp://localhost/notification-db-gui/Mongo Express for the notification database. Dev mode only.
Contracts DB GUIhttp://localhost/contracts-db-gui/Mongo Express for the contracts database. Dev mode only.
Chat DB GUIhttp://localhost/chat-db-gui/Mongo Express for the chat database. Dev mode only.
Grafanahttp://localhost/grafana/Metrics dashboards. Dev mode only.
Prometheushttp://localhost/prometheus/Raw metrics scraper. Dev mode only.

Production / Detached Mode

just stack start

Runs the stack using the base compose configuration in detached mode (-d). No hot-reloading, no database GUIs. Same exclude= syntax works here too.

Stopping & Resetting

# 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-sensor

Warning

just db clear permanently deletes all persistent Docker volumes.


Bootstrapping (First-Time Provisioning)

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.


Kubernetes Mode

See the Kubernetes Configuration page for the full reference. Quick summary:

First Time: Creating the Cluster

just k8s create

See Kubernetes Configuration for exactly what this creates and why (single-server k3d, Traefik disabled — Istio’s Gateway API is the local ingress path instead).

Deploying (Production images from GHCR)

# First-time full setup (create cluster → Gateway API + Istio → namespace → secrets → manifests):
just k8s setup

# Subsequent deploys — re-apply manifests:
just k8s apply

Deploying with Locally Built Images (k3d dev)

just k8s build      # build all images locally
just k8s load       # import into k3d — no GHCR needed
just k8s apply      # apply manifests

Updating a Single Service

# 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-service

Six of the twelve images build from the repo root, not their own directory

twin-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.

Managing Cluster Lifecycle

# 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 delete

Note

Use 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.

Verifying Cluster Health

# 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

Testing

# 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 integration

Note

just 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.

The agent’s golden-dataset evaluation

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.


Auditing & Dependency Checks

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 --affected

Note

just 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 EnvironmentLockfiles & 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).

Go projects aren’t part of the audit sweep yet

\.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.


Full Just Reference — Docker Compose

RecipeWhat 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 downGraceful shutdown, removes orphan containers.
just stack logs [service]Follow logs. Omit service to tail all.
just db clearDrop all databases (destructive).
just db clear-sensorDrop only the sensor database.
just test allRun all unit tests (moon-cached; unchanged services are instant).
just test affectedRun 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-integrationAgent-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 integrationRun backend integration tests.
just agent ingestRe-ingest all documentation into the agent’s knowledge base — run after editing anything under documentation/.
just lint allLint every service (moon-cached).
just lint affectedLint only services changed on the current branch.
just lint fixAuto-fix linting issues across every service.
just setup auditSecurity audit across all projects via moon run :audit (npm + uv + cargo).
just setup deps-checkLockfile-sync check across all projects via moon run :deps (npm ci + uv sync —locked + cargo check).
just setup clean-installWipe all node_modules + lockfiles and regenerate them for Linux, then reinstall.
just docs buildCompile 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 serveBuild both guides, then live-serve the whole landing-page/ site at :8080.

Debugging Tips

A service is responding with 502 Bad Gateway

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 service

Exclusion 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 frontend

If 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.


Windows: kubectl fails to connect after just k8s start

After 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 secrets

Or use just k8s setup which handles the ordering automatically.


k3d image pull fails (ImagePullBackOff)

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 load

Also check that the deployment manifest sets imagePullPolicy: IfNotPresent, not AlwaysAlways forces a remote pull even when the image exists locally.