Repository navigation
Architecture 09 Architecture Decisions
github-actions[bot] edited this page Oct 19, 2026
·
40 revisions
All decisions are recorded as ADRs in the ADR index.
| ADR | Decision |
|---|---|
| 0001 | OpenFGA instead of self-built authorization |
| 0002 | Self-built Identity service in FastAPI |
| 0003 | SvelteKit for the web app |
| 0004 | Monorepo with a uv workspace |
| 0005 | AGPL-3.0 and Apache-2.0 licensing |
| 0006 | PostgreSQL for all persistence |
| 0007 | NATS JetStream as event bus |
| 0008 | Traefik as gateway |
| 0009 | Command and event envelopes, subject naming and de-duplication |
| 0010 | Python 3.13 for all services and libraries |
| 0011 | Separate GPL-3.0 package for the PyMammotion adapter |
| 0012 | Mower control interface and safety rules |
| 0013 | Device-Gateway registry, sharding and per-mower concurrency |
| 0014 | Authorization model, tuple ownership and the mowdeck-authz client |
| 0015 | Tokens, sessions and key rotation |
| 0016 | Fleet commands and authorization per command kind |
| 0017 | Telemetry storage and retention |
| 0018 | Realtime WebSocket protocol |
| 0019 | Mammotion adapter: cloud first, BLE via ESPHome proxy, manual drive only over BLE, secret references, exact pins |
| 0020 | Web app architecture |
| 0021 | Browser authentication flow |
| 0022 | Notifications: rules derived from telemetry in the service, subscribers re-checked with can_view, outbox with SKIP LOCKED, channel plug-ins |
| 0023 | Map rendering and projection: Leaflet with a metric grid in local metres (CRS.Simple) by default, WGS84 only with configured tiles, conversion in the browser checked against golden vectors, Geoman deferred |
| 0024 | Scheduler: recurrence on zoneinfo, firing through Fleet's fleet.commands.issue on behalf of the owner with a re-check of can_control, SKIP LOCKED ticker, Open-Meteo rain pause behind a weather port |
| 0025 | Home Assistant integration: pure API client without shared Python code, API token exchange, push over the Realtime WebSocket with polling fallback, dedicated HACS repository published from the monorepo |
| 0026 | Manual driving: session opened once over the Realtime WebSocket and recorded by Fleet (fleet.drive.start/stop, can_drive, one open session per mower), frames published by Realtime as ManualDrive commands (rate limited, ttl_ms 1000), the Device-Gateway controller stays the only safety authority |
| 0027 | Health history computed incrementally in Telemetry's state consumer (UTC days, blade hours heuristic, blade reset, telemetry.blade_hours.threshold), theft alarms in Notifications (lift, unexpected dock leave, geofence exit; critical, bypass quiet hours, acknowledge silences an hour) |
| 0028 | Plans as data seeded from infra/entitlements/plans.yaml, MOWDECK_ENTITLEMENTS_BILLING_ENABLED (off: every account unlimited), hierarchical feature keys from module descriptors, entitlements.check with a per-account snapshot cache in the kit client (no answer: the local billing switch decides), grace and expired keep only view and emergency stop, operator endpoints on platform:main
|
| 0029 | Helm chart and release pipeline: one umbrella chart with a template per service and the Compose settings under the same names, NATS and OpenFGA as optional official subcharts, PostgreSQL through CloudNativePG (one database and role per service) or external, migrations as hooks (external database) or per-revision Jobs (in-cluster) with pods waiting for their schema and model, Ingress resources instead of Traefik CRDs, multi-arch images through QEMU and the chart as an OCI artifact in GHCR from tags only |
| 0030 | Mowing patterns as plug-ins in the pure library mowdeck-patterns (Shapely, entry point group mowdeck.patterns, settings as JSON Schema), one validation chain for every plan, chains and templates, plans inline in StartMow.spot_plan (encoded segments, SHA-256 hash, optional native job), execution by capabilities.custom_paths or the native job, Fleet gates spot_mowing and records the hash, coverage from Telemetry's track over tracks.positions.query, the Planner owns only templates and a snapshot directory |
| 0031 | Billing as an optional service (Compose profile billing, Helm services.billing.enabled, not started when disabled; the web app probes GET /billing/status), PaymentProvider with a Stripe adapter (Checkout and customer portal, one customer per account, prices as data in infra/billing/prices.yaml) and a fake, webhooks verified by signature and processed once (webhook_events ledger, stale events ignored), Stripe statuses mapped to plan statuses set in Entitlements over entitlements.plan.set (source=billing), grace from the end of the last paid period, owner only (can_manage_billing) |
| 0032 | Live camera over Agora: the browser joins the Mammotion camera channel directly with the Agora Web SDK (no media through MowDeck); the adapter fetches the stream token, the Device-Gateway answers gateway.mower.<id>.camera.session, Fleet checks can_view and the plan feature camera and audits; the Agora domains enter the CSP only with MOWDECK_WEB_CAMERA_ENABLED=true
|
| 0033 | Vision-assisted recovery: a vision model (any family through LiteLLM; per-user providers with encrypted keys, an optional installation model behind ai_recover_hosted) proposes at most six drive steps, a user with can_drive approves, the service drives them through fleet.drive.* on the approver's behalf (origin: vision) at 5 Hz with ttl_ms 1000 and blades off, aborts on emergency stop, sensor stop, geofence, missing state or cancellation, verifies, at most three rounds; drive_recoverable errors are the only ones the controller lets drive frames through |
| 0034 | Linked cloud accounts: Fleet stores Mammotion accounts per MowDeck account (password AES-256-GCM under MOWDECK_CREDENTIAL_ENCRYPTION_KEY), the Device-Gateway logs in and lists the devices on gateway.cloud.discover, cloud mowers reference a linked account and its device, one PyMammotion session per account shared by its mowers and the discovery, one physical mower (IoT id) in one MowDeck account installation-wide (409 mower-already-registered), old cloud mowers moved on start |
Mirrored from docs/architecture/09-architecture-decisions.md at commit 8f3f7119bf4d91d9f8099b4597d37bec6990bf2f. Edit the file in the repository, not this wiki page.
Mirrored from the docs/ directory at commit 8f3f7119bf4d91d9f8099b4597d37bec6990bf2f. 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
- ADR 0045: Camera relay
- Development
- Operations
- Spikes
- Guides
- Original plan (German)