/

Developer Guide

Architecture Overview

CrowdVision is a distributed, event-driven microservices system. The frontend is fully separated from the backend, and the backend services are isolated from one another, so the platform can scale parts independently, tolerate partial failure, and enforce strict data boundaries.

This page gives the whole-system picture — the components and the two deployment models. Inter-service communication and the real-time data path are covered in Communication & Data Flow; the cluster specifics in Deployment & Kubernetes; the Istio mesh migration in Service Mesh Architecture.


Component Landscape

Libraries

Shared code embedded directly into other services — not independently deployed, no Dockerfile of their own.

LibraryRoleStack
auth-contractsShared Kernel: the StandardClaims/Membership token shape and the role-weight ladder (roles.json). Consumed by auth-middleware, auth-policy, claims-gateway, tenancy-service.Go module
auth-middlewareTwo verification strategies: RequireAuthentication (full JWT verify against JWKS) and RequireMeshClaims (trusts the mesh-injected x-gateway-claims header). Consumed by claims-gateway and tenancy-service (the only Go HTTP services).Go module
auth-policyCedar authorization bundle (schema.cedarschema, policy.cedar, fixtures/conformance.json) embedded and evaluated locally by each language: cedar-go (Go, tenancy-service), cedar-policy crate via include_str! at compile time (Rust, twin-service), cedarpy at runtime (Python, agent-service). One shared bundle, three independent embeddings — not a package import, a sibling directory each build copies in.Go module + Rust/Python bindings

Services

Independently deployable, each with its own container image.

ComponentRoleStack
KeycloakIdentity provider for every tier — authentication, credentials, MFA, brokered/BYO-IdP login. See Identity & Tenancy Architecture.Ops (Keycloak 26)
claims-gatewayVerifies the IdP token, resolves tenancy, mints the internal token every service trustsGo
tenancy-serviceDomains & memberships — who belongs to which organization, with what roleGo / PostgreSQL
registry-serviceOrganization signup & lifecycle (control plane; never deployed in a cell)Go / PostgreSQL
provisionerReconciles pending organizations into running tenancy (pooled tier today). No external route — internal reconcile loop only.Go
twin-serviceBuilding Management — the spatial modelRust / Axum / MongoDB
sensor-serviceTelemetry Ingestion — readings & thresholdsNode.js / Express / MongoDB
contracts-serviceTelemetry Distribution — per-building filteringRust / Axum / MongoDB
notification-serviceAlerting — push deliveryNode.js / Express / MongoDB
socket-serviceReal-time transport to the browserNode.js / Socket.IO
chat-servicePersists multi-turn chat sessions and orchestrates calls to agent-serviceNode.js / MongoDB
agent-serviceAssistant — maintained separately; see its referencePython / PostgreSQL (pgvector)
clientThe single-page applicationVue 3 / Vite
GatewaySingle entry point, routes by URL prefixCaddy (compose) / Istio Gateway API (k8s)
Service MeshKubernetes only: Istio in ambient mode — ztunnel gives every pod L4 mTLS, edge RequestAuthentication verifies the JWT once, AuthorizationPolicy enforces coarse authZ. Docker-compose’s equivalent is Caddy’s forward_auth against claims-gateway’s /verify. See Service Mesh Architecture.Istio (ambient)
BrokerPublish/subscribe backbone for telemetry and alertsRedis
DatastoresOne database per stateful serviceMongoDB (sensor-service, notification-service, chat-service — Node.js; twin-service, contracts-service — Rust), PostgreSQL (agent-service, tenancy-service, registry-service, Keycloak)

The agent-service runs as part of the system but sits outside this guide’s microservice set; it appears here for completeness only.


Deployment Model: Docker Compose (Local Development)

All incoming external traffic enters the system through a single Caddy reverse-proxy gateway exposed on port 80. Caddy strips the service prefixes from the URLs and proxies the requests to the appropriate target containers on the internal Docker bridge network. For protected routes, Caddy’s forward_auth calls claims-gateway’s /verify and copies the resulting X-Gateway-Claims header onto the request — the docker-compose equivalent of Istio’s RequestAuthentication + outputPayloadToHeader in the Kubernetes model below.

graph TD
    Browser["Browser / Vue Client"] -->|HTTP + WS| Caddy["Caddy Gateway :80"]
    IoT["IoT Sensors (Simulated)"] -->|HTTP POST| Caddy

    subgraph gateway-net [Shared Gateway Network]
        Caddy -->|/gateway/*| CG["claims-gateway :3000"]
        Caddy -->|"/tenancy/* (forward_auth)"| TS["tenancy-service :3000"]
        Caddy -->|"/twin/* (forward_auth)"| Twin["twin-service :3000"]
        Caddy -->|"/notification/* (forward_auth)"| Notif["notification-service :3000"]
        Caddy -->|"/socket.io/* (forward_auth)"| Socket["socket-server :3000"]
        Caddy -->|"/sensor/* (forward_auth, ingest exempt)"| Sensor["sensor-service :3000"]
        Caddy -->|"/chat/* (forward_auth)"| Chat["chat-service :3000"]
        Caddy -->|/agent/* - no gate| Agent["agent-service :3000"]
        Caddy -->|/contracts, /contracts/*| Contracts["contracts-service :3000"]

        Notif -->|PUBLISH| Redis[("Redis Broker")]
        Socket -->|SUBSCRIBE| Redis
        Contracts --- Redis
    end

    RegSvc["registry-service :3000"]
    ProvSvc["provisioner<br/>(no inbound route)"]
    ProvSvc -->|"HMAC-signed, internal"| RegSvc
    ProvSvc -->|"HMAC-signed, internal"| TS

    subgraph tenancy-net [Tenancy Private Network]
        TS --- TenancyDB[("tenancy-db PostgreSQL")]
    end

    subgraph registry-net [Registry Private Network]
        RegSvc --- RegistryDB[("registry-db PostgreSQL")]
    end

    subgraph twin-net [Twin Private Network]
        Twin --- TwinDB[("twin-db MongoDB")]
    end

    subgraph notification-net [Notification Private Network]
        Notif --- NotifDB[("notification-db MongoDB")]
    end

    subgraph sensor-net [Sensor Private Network]
        Sensor --- SensorDB[("sensor-db MongoDB")]
    end

    subgraph chat-net [Chat Private Network]
        Chat --- ChatDB[("chat-db MongoDB")]
    end

    subgraph contracts-net [Contracts Private Network]
        Contracts --- ContractsDB[("contracts-service-db MongoDB")]
    end

    subgraph agent-net [Agent Private Network]
        Agent --- AgentDB[("agent-db PostgreSQL / pgvector")]
    end

Network Segregation & Database Isolation

Each stateful microservice is attached to two networks:

This is the intended database-per-service boundary: no service is meant to reach another service’s database directly, only through an API call or an event.


Deployment Model: Kubernetes (Production / Staging)

In Kubernetes mode, Caddy is replaced by an Istio Gateway (Gateway API, gatewayClassName: istio) fronting an Istio ambient mesh: a per-node ztunnel DaemonSet gives every pod L4 mTLS and workload identity with no sidecar and no pod restarts, PeerAuthentication enforces STRICT mTLS across the crowdvision namespace, and a RequestAuthentication on the gateway verifies the claims-gateway-issued JWT exactly once — the validated payload is injected as a single x-gateway-claims header via outputPayloadToHeader, and every downstream service decodes that header instead of re-verifying the JWT itself. The full phased migration is documented in Service Mesh Architecture.

graph TD
    Browser["Browser / Vue Client"] -->|HTTP port 80| LB["k3d LoadBalancer"]
    Simulator["Sensor Simulator (external Docker)"] -->|HTTP POST| LB

    LB --> IGW["Istio Gateway (Envoy)<br/>auto-provisioned: crowdvision-gateway-istio"]

    subgraph crowdvision ["Namespace: crowdvision — Istio ambient, ztunnel mTLS STRICT on every pod"]
        IGW -->|"RequestAuthentication: JWT verified once<br/>outputPayloadToHeader to x-gateway-claims"| HR{{"Gateway API HTTPRoutes"}}

        HR -->|/gateway| CGSvc["claims-gateway"]
        HR -->|/tenancy| TSSvc["tenancy-service"]
        HR -->|/twin| TwinSvc["twin-service"]
        HR -->|/sensor| SensorSvc["sensor-service"]
        HR -->|/notification| NotifSvc["notification-service"]
        HR -->|/chat| ChatSvc["chat-service"]
        HR -->|/socket.io| SocketSvc["socket-service"]
        HR -->|"/agent (excluded from JWT gate)"| AgentSvc["agent-service"]
        HR -->|/contracts| ContractsSvc["contracts-service"]
        HR -->|"/ catch-all"| ClientSvc["client"]

        RegSvc["registry-service"]
        ProvSvc["provisioner<br/>(no Gateway route)"]
        ProvSvc -->|"HMAC-signed, in-mesh"| RegSvc
        ProvSvc -->|"HMAC-signed, in-mesh"| TSSvc

        TSSvc --- TenancyDB[("tenancy-db StatefulSet")]
        RegSvc --- RegistryDB[("registry-db StatefulSet")]
        TwinSvc --- TwinDB[("twin-db StatefulSet")]
        NotifSvc --- NotifDB[("notification-db StatefulSet")]
        SensorSvc --- SensorDB[("sensor-db StatefulSet")]
        ChatSvc --- ChatDB[("chat-db StatefulSet")]
        ContractsSvc --- ContractsDB[("contracts-service-db StatefulSet")]

        SocketSvc -->|SUBSCRIBE| RedisPod[("Redis Deployment")]
        NotifSvc -->|PUBLISH| RedisPod
        ContractsSvc --- RedisPod
    end

A key difference in the Kubernetes model is how the Sensor Simulator integrates. Because the simulator is an external Docker container (not a cluster workload), it reaches the cluster through the load balancer’s public port, just like a browser. See the External Sensor Simulator page. The cluster internals — StatefulSets, Secrets, Gateway API routes — are detailed in Deployment & Kubernetes; the mesh internals (ztunnel, RequestAuthentication, AuthorizationPolicy, Cedar) in Service Mesh Architecture.

agent-service is deliberately excluded from the mesh’s AuthorizationPolicy-enforced JWT gate: it also serves a local-dev evaluation bypass token that never holds a real gateway JWT, so it keeps checking for the mesh claims header first and falls back to its own token verification in-process.


Routing Table

The routing table is identical across both deployment models. Only the component enforcing it differs (Caddy vs Istio Gateway API).

External Path PrefixInternal Target ServicePurpose
/gateway/claims-gatewayToken exchange, /me, logout, JWKS.
/tenancy/tenancy-serviceDomains, subdomains, memberships, invite codes.
/twin/twin-serviceBuilding models, room CRUD, spatial data.
/notification/notification-serviceProcessing sensor events, triggering Web Push alerts.
/sensor/sensor-serviceIngesting time-series sensor readings (people count, temperature). /sensor/ingest is exempt from the auth gate — it is device/simulator-facing and carries no user token.
/chat/chat-servicePersisted multi-turn chat sessions, forwarding to agent-service.
/agent/agent-serviceTool-calling assistant for live building data and documentation search. Not gated by the mesh/gateway JWT check — see the exclusion note above.
/contracts/contracts-servicePer-building telemetry filtering preferences. Not gated by the auth check.
/socket.io/socket-serviceMaintaining persistent WebSocket connections to the frontend.
/clientServes the Vue 3 SPA. Catch-all, must be defined last.

Dev Mode

Developer Mode Only: when running just stack dev, Caddy also exposes database GUIs at /twin-db-gui/*, /sensor-db-gui/*, /notification-db-gui/*, /contracts-db-gui/*, and /chat-db-gui/*. These are not present in Kubernetes deployments for security reasons.