Architecture 09 Architecture Decisions - lukislp/mowdeck GitHub Wiki
9. Architecture decisions
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 cb46ac5074dee783a333b60ecc6a9b05a1c7cf33. Edit the file in the repository, not this wiki page.