Repository navigation
Dev Service Template
Every MowDeck service is generated from the Copier template in templates/service. The template gives a new service the layers, the operational endpoints, tests, a Dockerfile, a Compose block and a README in the agreed structure, so a new service starts compliant with the operating rules (Twelve-Factor, non-root, read-only file system, health endpoints, graceful shutdown). The Device-Gateway was the first service generated from it.
uv run copier copy --trust templates/service services/<service-name>| Question | Default | Meaning |
|---|---|---|
service_name |
none | kebab-case, e.g. reports; distribution name, Compose service and HTTP path /api/v1/<service_name>
|
package_name |
service_name in snake_case |
Python package under src/
|
description |
MowDeck <name> service. |
one sentence |
needs_db |
false |
own PostgreSQL database with SQLAlchemy and Alembic |
http_port |
8000 |
port inside the container (1024 or higher) |
permissions |
<package>.view: View <name> data |
YAML mapping of permission key to description; the first one protects the example endpoint |
--trust is needed because the template computes derived values; --defaults with -d key=value answers everything on the command line. Then:
- Add the service to the root
pyproject.toml: the distribution name to thedevdependency group and<name> = { workspace = true }to[tool.uv.sources], souv syncinstalls it editable for tests and mypy. -
uv sync,uv run poe lint,uv run pytest services/<name>. - Paste
compose.snippet.ymlintoinfra/compose/docker-compose.yml(for a database service also add the database and role toinfra/compose/init/01-databases.sql).
services/<name>/
pyproject.toml workspace member, AGPL-3.0-only, depends on mowdeck-service-kit and mowdeck-contracts
README.md purpose, interfaces, events, settings, local start, tests
Dockerfile multi-stage uv build, non-root, read-only friendly, HEALTHCHECK
Dockerfile.dockerignore allow list for the repository root build context
compose.snippet.yml Compose block with Traefik labels for /api/v1/<name>
alembic.ini (needs_db) for creating revisions during development
src/<package>/
main.py build_app(settings, bus=..., principal_resolver=...) and main()
settings.py Settings(ServiceSettings) with the service defaults
module.py ModuleDescriptor with the declared permissions
domain/ models and rules, no I/O
application/ use cases and ports (Protocols)
adapters/api/ FastAPI routers; GET /api/v1/<name>/ping requires the first permission
adapters/bus/ subscriptions and publishers
adapters/db/ (needs_db) tables, migrate entry point, Alembic migrations
tests/
test_<package>_api.py health, module descriptor, 401, 403, 200
test_<package>_app.py startup and shutdown with the in-memory bus
| Layer | May import | Must not |
|---|---|---|
domain |
standard library, mowdeck_contracts
|
do I/O, import FastAPI, NATS, SQLAlchemy or adapters |
application |
domain, its own ports, libraries without I/O (mowdeck_control) |
import adapters or main; it talks to the outside only through ports |
adapters |
application, domain, frameworks |
contain business rules; they translate and implement ports |
main |
everything | contain logic beyond wiring |
Ports are typing.Protocol classes in application/ports.py. Tests replace adapters with fakes through build_app arguments, never by patching modules.
-
Configuration only from
MOWDECK_*variables orMOWDECK_<NAME>_FILEsecret files (Settings.from_env()); every field has a docstring, which is the settings reference. -
Operations:
/healthz,/readyz,/metricsand/.well-known/mowdeck-modulefrom the kit; JSON logs on stdout; graceful shutdown hooks withinMOWDECK_SHUTDOWN_GRACE_SECONDS. -
Image: built from the repository root with
uv sync --frozen --no-dev --no-editable --package <name>, so only the service and the workspace libraries it depends on are installed. Runs as userapp(uid 10001), setsPYTHONDONTWRITEBYTECODE=1and writes nothing at runtime: Compose runs it withread_only: trueand a tmpfs at/tmp, Helm withreadOnlyRootFilesystem. No architecture specific steps, so the same Dockerfile builds amd64 and arm64. The ignore file isDockerfile.dockerignorebecause the build context is the repository root (BuildKit reads the ignore file next to the Dockerfile). -
Migrations (needs_db) run as a separate step before the service:
python -m <package>.adapters.db.migrate(Compose: one-shot<name>-migrateservice, Helm: a Job). The Alembic configuration is built in code, so the image needs noalembic.ini. Migrations must stay backward compatible for rolling updates. -
Tests have unique file names (
test_<package>_*.py) and noconftest.py(mypy treats equally named modules of different packages as duplicates); shared helpers go into a uniquely named module next to the tests. -
Permissions: every endpoint uses
require_permission(...); endpoints touching account data filter by the principal's account and need a cross-tenant test (see the kit README).
- README in the template structure (purpose, interfaces, events, settings, local start, tests) and kept current.
- Tests: the generated ones plus tests for every use case; cross-tenant tests for every endpoint and handler touching account data; coverage of at least 85 %.
-
uv run poe lintanduv run poe testgreen; the service is listed in the rootpyproject.toml. - Module descriptor declares every permission and every produced event.
-
compose.snippet.ymladded toinfra/compose/docker-compose.yml,uv run poe compose-checkgreen, anddocker compose up -d --waitreports the service healthy. - A template in the umbrella chart
infra/helm/mowdeck(templates/<name>.yamlcallingmowdeck.workloadand, with a database,mowdeck.migrationJob; settings invalues.yamlunderconfig, secrets as_FILEfiles),sh infra/helm/check.shgreen (Helm operations).tools/repo-checks/tests/test_helm.pyfails until the Compose service has its chart template. - Docs: a line in building blocks, runtime scenarios in runtime view where the service takes part, contracts documented in
mowdeck-contracts. - An ADR for every architecture decision the service makes.
Change the template and the services generated from it together. tools/repo-checks/tests/test_template.py renders both variants (with and without database) on every test run; for larger changes also render into services/, run lint and the generated tests, and delete the render again. Services keep their answers in .copier-answers.yml.
Mirrored from docs/dev/service-template.md at commit a117684672263ba4f0b7e2e3538233456122d24e. Edit the file in the repository, not this wiki page.
Mirrored from the docs/ directory at commit a117684672263ba4f0b7e2e3538233456122d24e. Edit the files in the repository, not this wiki.
MowDeck documentation
- Home
- Phases
- Architecture
-
Decisions (ADR)
- Architecture decisions
- ADR 0001: OpenFGA instead of self-built authorization
- ADR 0002: Self-built Identity service in FastAPI instead of Keycloak or Zitadel
- ADR 0003: SvelteKit for the web app
- ADR 0004: Monorepo with a uv workspace
- ADR 0005: AGPL-3.0 for services and apps, Apache-2.0 for libraries
- ADR 0006: PostgreSQL for all persistence, no SQLite
- ADR 0007: NATS JetStream as event bus
- ADR 0008: Traefik as gateway
- ADR 0009: Command and event envelopes, subject naming and de-duplication
- ADR 0010: Python 3.13 for all services and libraries
- ADR 0011: Separate GPL-3.0 package for the PyMammotion adapter
- ADR 0012: Mower control interface and safety rules
- ADR 0013: Device-Gateway registry, sharding and per-mower concurrency
- ADR 0014: Authorization model, tuple ownership and the mowdeck-authz client
- ADR 0015: Tokens, sessions and key rotation
- ADR 0016: Fleet commands and authorization per command kind
- ADR 0017: Telemetry storage and retention
- ADR 0018: Realtime WebSocket protocol
- ADR 0019: Mammotion adapter transports, manual drive and secrets
- ADR 0020: Web app architecture
- ADR 0021: Browser authentication flow
- ADR 0022: Notifications: rules, recipients and delivery
- ADR 0023: Map rendering and projection
- ADR 0024: Scheduler: recurrence, rain pause and command issuing
- ADR 0025: Home Assistant integration as a pure API client
- ADR 0026: Manual drive channel over the Realtime WebSocket
- ADR 0027: Health history and theft alarm
- ADR 0028: Entitlements: plans as data and enforcement
- ADR 0029: Helm chart and release pipeline
- ADR 0030: Mowing patterns as plug-ins and the Planner
- ADR 0031: Billing with a Stripe adapter
- ADR 0032: Live camera over Agora, joined directly by the browser
- ADR 0033: Vision-assisted recovery with human approval
- ADR 0034: Linked cloud accounts and device discovery
- ADR 0035: Server-side camera frames and automatic AI analysis on errors
- ADR 0036: Native app shells with Capacitor, loading the web app from the server
- ADR 0037: Camera panorama calibration in the browser, with the AI supplying points only
- ADR 0038: Dual link per mower and firmware updates
- ADR 0039: Spot mowing by MowDeck-driven paths
- ADR 0040: Link heartbeat for online status
- ADR 0041: Spot runs from the dock: undock, transit and return
- ADR 0042: Drive calibration
- ADR 0043: Turn styles
- ADR 0044: Drive view with camera
- Development
- Operations
- Spikes
- Guides
- Original plan (German)