/

Developer Guide

Contributing & Golden Rules

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.


The polyglot monorepo

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:

Dependencies are not hoisted

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.


Architectural golden rules

These rules preserve the boundaries the architecture depends on. A contribution that violates one will not be accepted, even if it passes CI.

RuleWhat it means in practice
Respect the database-per-service boundaryA 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 statelessHold 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 serviceA 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 lightweightAvoid adding a global store (Pinia/Vuex) unless genuinely necessary; prefer composables and local state.
Test what you changeEvery 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 miseNever rely on the ambient PATH. Use just recipes (which wrap mise exec --) or mise exec -- <tool> directly. Register any new package in &#46;moon/workspace.yml.

The contribution workflow

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"]
  1. Branch from master using the branch-naming convention, see Naming Conventions.
  2. Bootstrap the repository once with just setup install (which runs mise install first).
  3. Develop against the local stack with just stack dev, see Docker Compose Orchestration.
  4. Format and lint with just lint fix, then verify with just lint affected.
  5. Test with just test affected (or just test all for the full suite).
  6. Verify dependencies with just setup deps-check (lockfile sync) and just setup audit (vulnerabilities) — these mirror the ci-deps and ci-audit gates.
  7. Commit using Conventional Commits, see Commits & Release Please.
  8. Open a pull request to master. The single required check is ci-gate / ci-passed.

Pre-push checklist

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)

The commit message is part of the build

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.


Why release-please’s extra-files instead of Lerna

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.