Developer Guide
This page is the contract for working in the CrowdVision repository: how the monorepo is organised, the architectural rules contributions must respect, and the day-to-day workflow from clone to merged pull request. The related conventions are detailed on their own pages: Naming Conventions, Commits & Release Please, and CI/CD Pipeline.
CrowdVision is a polyglot monorepo: a Vue frontend, four Node services in TypeScript (chat, notification, sensor, socket), two Rust services (contracts-service, twin-service), a Python agent-service, and four Go services forming the identity/tenancy control plane (claims-gateway, tenancy-service, registry-service, provisioner) — orchestrated by moon (task caching and affected-detection) behind a root Justfile. See Naming Conventions for the per-language style each of these follows.
Two structural facts shape everything you do:
package.json and no npm workspaces. Each Node service installs its own node_modules. tooling/ holds only the shared ESLint flat config, @crowdvision/eslint-config, at tooling/eslint-config/.\.moon/workspace.yml, and the toolchain derives everything else from it. See The Toolchain.When you add a Node dependency, install it inside that service’s directory (cd backend/chat-service && npm install <pkg>). Because that npm install resolves the lockfile for your operating system, regenerate it for Linux before committing — just setup clean-install, or per-service npm install --prefix backend/chat-service --package-lock-only --cpu=x64 --os=linux — otherwise CI’s npm ci fails on missing Linux optional packages. Run just setup deps-check before pushing. Rust and Go dependencies aren’t hoisted either, but don’t have this platform-resolution pitfall: cargo add <crate> / go get <module> inside the service directory is enough.
These rules preserve the boundaries the architecture depends on. A contribution that violates one will not be accepted, even if it passes CI.
| Rule | What it means in practice |
|---|---|
| Respect the database-per-service boundary | A service never opens a connection to another service’s database. If a service needs data it does not own, it reads it from the verified identity token, asks the owning service over its API, or reacts to a broker event. Never configure a cross-database Mongoose connection. |
| Keep services stateless | Hold no session state in a service; all caller authority travels in the signed token, so any instance can serve any request. Durable state belongs only in databases and the broker. |
| One bounded context per service | A service realises exactly one context from Domains & Contexts. Logic that belongs to another context goes in that context’s service, reached through its contract. |
| Keep the frontend lightweight | Avoid adding a global store (Pinia/Vuex) unless genuinely necessary; prefer composables and local state. |
| Test what you change | Every backend service has a test suite in its language’s idiomatic location (__tests__/ for the TypeScript services, in-module #[cfg(test)] plus tests/ for the Rust services, *_test.go alongside the code for the Go services). New behaviour must be covered, and just test all must pass before you open a pull request. |
| Route every tool through mise | Never rely on the ambient PATH. Use just recipes (which wrap mise exec --) or mise exec -- <tool> directly. Register any new package in .moon/workspace.yml. |
flowchart LR
A["Branch from master"] --> B["just setup install"]
B --> C["Code + just stack dev"]
C --> D["just lint fix"]
D --> E["just test affected"]
E --> F["just setup deps-check + just setup audit"]
F --> G["Conventional commit"]
G --> H["Open PR"]
H --> I["ci-gate / ci-passed"]
I --> J["Merge to master"]master using the branch-naming convention, see Naming Conventions.just setup install (which runs mise install first).just stack dev, see Docker Compose Orchestration.just lint fix, then verify with just lint affected.just test affected (or just test all for the full suite).just setup deps-check (lockfile sync) and just setup audit (vulnerabilities) — these mirror the ci-deps and ci-audit gates.master. The single required check is ci-gate / ci-passed.Running the same gates locally that CI runs avoids a slow round-trip through a failed pipeline.
just lint affected # mirrors ci-lint
just test affected # mirrors the per-service ci-* test legs
just setup deps-check # mirrors ci-deps (npm ci / uv sync --locked / cargo check)
just setup audit # mirrors ci-audit (npm / uv / cargo audit — no Go leg yet)Nothing in CI rejects a malformed commit message — release-please just derives the next version directly from whatever prefix each message has, silently treating anything non-conforming as no-release. Follow Conventional Commits because the release pipeline depends on it, not because a check enforces it. See Commits & Release Please.
Release-time version bumps across every manifest are applied by release-please-config.json’s extra-files glob patterns (see Commits & Release Please, rather than Lerna or npm workspaces.
Lerna 9 dropped its own package-discovery field and now delegates entirely to npm workspaces. npm workspaces cannot point upward with \../ — workspace paths must be strict descendants of the directory owning the package.json. Because tooling/ sits inside the repository rather than above the services, a tooling/package.json declaring "workspaces": ["../frontend", "../backend/*"] is rejected by npm outright. Tracking a single release-type: simple package at the repo root and listing every other manifest as a glob-matched extra-file sidesteps the constraint entirely — no workspaces, no Lerna discovery involved.