Architecture Overview¶
GreenThumb is a Cloud + Pi hybrid system: one or more Raspberry Pi 5 nodes each run a fully self-contained greenhouse controller (local database, API, hardware drivers, local dashboard), while a shared cloud backend aggregates data, manages identity, and allows remote administration across the fleet.
System Diagram¶
flowchart TB
subgraph PI["🍓 Raspberry Pi 5 (per greenhouse)"]
direction TB
CTRL["🎮 controller"]
API["🌐 microcontroller-api :8080"]
PIDB[("🗄️ PostgreSQL (local)")]
BG["⚙️ Background tasks\nsensor-persist · photo-capture · cloud-sync\nliveness · data-prune · heartbeat-watchdog"]
DASH["🖥️ local-dashboard :80"]
end
subgraph HW["🔌 Hardware"]
CAM["📷 USB Camera"]
SENSORS["🌡️ Sensors\ne.g. AHT10 · BMP280 · TSL2561"]
ACTUATORS["💡 Actuators\nrelay bank: grow light · fan · pumps · dosing pumps"]
end
subgraph CLOUD["☁️ Cloud"]
direction TB
GW["🔀 gateway :80"]
FASTAPI["🐍 greenthumb-api :8000"]
AUTH["🔐 auth-service :8081\n(Java / Spring Boot)"]
ACCT["👤 account-service :8082\n(Java / Spring Boot)"]
SUPA["📦 PostgreSQL 17 + TimescaleDB\n(self-hosted) + R2"]
end
BROWSER["🖥️ Admin Browser\n(admin-dashboard SPA)"]
MODELS["📦 greenthumb-models\n(shared Python package)"]
%% Controller ↔ API
CTRL -->|"GET /state/ (default every 15 s)"| API
CTRL -->|"POST /actuator/{id}/command"| API
CTRL -->|"POST /state/heartbeat"| API
%% API ↔ Hardware
API --> CAM
API --> SENSORS
API --> ACTUATORS
%% API ↔ local DB
API --> PIDB
BG --> PIDB
%% Dashboard ↔ API
DASH -->|"proxy /state /data /actuator /camera /settings"| API
%% Pi ↔ Cloud
BG -->|"pull config · push measurements, state,\nlogs, photos (over WireGuard)"| GW
GW --> FASTAPI
GW --> AUTH
AUTH --> ACCT
FASTAPI --> SUPA
AUTH --> SUPA
ACCT --> SUPA
%% Admin browser ↔ Gateway
BROWSER --> ADM["admin-dashboard (nginx)"]
ADM -->|"/admin /auth"| GW
%% Shared library
MODELS -.->|"models · sync schemas"| API
MODELS -.->|"models · sync schemas"| FASTAPI
The admin dashboard and cloud API are not publicly reachable yet; only the landing site is public.
Two Tiers of Storage¶
| Location | Database | Purpose |
|---|---|---|
| Pi-local | PostgreSQL (inside Docker) | Real-time readings, actuator state, cultivations and control rules, photo metadata |
| Cloud | Self-hosted PostgreSQL 17 + TimescaleDB | Fleet-wide history, aggregated measurements, user accounts, device registry |
Measurements, logs, photos and device-owned state (cultivations, phases, control rules) flow Pi → Cloud on every sync cycle. Configuration flows Cloud → Pi at the start of every cycle: the shared catalog is overwritten, device configuration converges by last-write-wins. When the cloud is unreachable the Pi keeps running from its local database.
Package Architecture¶
The Python codebase is split into two installable packages:
| Package | Location | Contents |
|---|---|---|
greenthumb (repo greenthumb-models) |
rasp5/greenthumb-models/ |
SQLModel table definitions, Pydantic DTOs, sync schemas — imported by both the Pi API and the cloud API |
greenthumb-rpi5 |
rasp5/greenthumb-rpi5/ |
Hardware drivers (sensors, actuators, camera), DeviceManager — Pi only |
Services¶
Raspberry Pi (per device)¶
| Service | Port | Role |
|---|---|---|
api (microcontroller-api) |
8080 | Hardware control, REST API, MJPEG stream |
controller |
— | Sense-Think-Act loop (default every 15 s, tunable per device) |
local-dashboard |
80 | React SPA served by nginx, proxied to api |
db |
5432 (published on the Pi host) | Local PostgreSQL |
watchtower |
— | Checks for new images hourly; updates local-dashboard automatically; api/controller updates are promoted by hand |
Six asyncio background tasks run inside the API process; their intervals are per-device runtime settings (GET/PATCH /settings/runtime):
- sensor-persist (
sensor_interval_s, 300 s) - photo-capture (
photo_interval_s, 4 h) - cloud-sync (
sync_interval_s, 3600 s): pulls config, then pushes measurements, device state, actuator and calibration logs, photos - liveness (pings the cloud)
- data-prune (prunes old local data)
- heartbeat-watchdog (
heartbeat_timeout_s, 60 s): puts actuators in a safe state when the controller stops sending heartbeats
Cloud¶
| Service | Port | Role |
|---|---|---|
gateway |
80 | Spring Cloud Gateway: /admin/**, /sync/** and / → greenthumb-api; /auth/** → auth-service |
greenthumb-api |
8000 | FastAPI — admin CRUD + Pi sync endpoints |
auth-service |
8081 | Java Spring Boot — JWT login & validation |
account-service |
8082 | Java Spring Boot: user accounts; internal only (called by auth-service, not routed by the gateway) |
dashboard (admin-dashboard) |
80 | React SPA served by nginx; proxies /admin/ and /auth/ to the gateway |
landing (landing-page) |
80 | Public landing site, React app served by nginx |
The admin-dashboard and landing-page React apps are served by their own nginx containers in the same Compose stack. Only the landing site is public today.
Authentication Model¶
| Actor | Mechanism |
|---|---|
| Human users (admin dashboard) | JWT issued by auth-service at login and sent as an HttpOnly access_token cookie (or Authorization: Bearer). greenthumb-api validates it on every request via GET /auth/validate, which returns the user id and current role (user or admin). |
| Pi devices (sync calls) | Per-device device_token (secrets.token_urlsafe(32)) sent as Authorization: Bearer <token>; validated against the device table |
Data Flow: Sense-Think-Act Loop¶
sequenceDiagram
participant CTRL as Controller
participant API as microcontroller-api
participant HW as Hardware
participant DB as Local PostgreSQL
loop Every 15 s (default, tunable per device)
CTRL->>API: GET /state/
API->>HW: Read sensors
HW-->>API: Sensor values
API->>DB: Load variable labels
DB-->>API: variables map
API-->>CTRL: {sensors, sensor_values, variables, control_rules, actuators, effects, active_phase, safety_mode, timezone}
Note over CTRL: Evaluate control rules
CTRL->>API: POST /actuator/{id}/command
API->>HW: Set actuator state
CTRL->>API: POST /state/heartbeat
end
Data Flow: Cloud Sync¶
sequenceDiagram
participant BG as cloud-sync task (Pi)
participant GW as Gateway
participant API as greenthumb-api (cloud)
participant R2 as Cloudflare R2
loop Every sync_interval_s (default 3600 s)
BG->>GW: GET /sync/devices/{id}/config
GW->>API: forward
API-->>BG: shared catalog + device configuration
BG->>GW: POST /sync/devices/{id}/measurements
BG->>GW: POST /sync/devices/{id}/state
BG->>GW: POST /sync/devices/{id}/actuator-logs
BG->>GW: POST /sync/devices/{id}/calibration-logs
BG->>GW: POST /sync/devices/{id}/photos (multipart)
GW->>API: forward
API->>R2: upload photo
end
The Pi never contacts R2 directly: it sends photos to the cloud API, which uploads them.
Technology Decisions¶
| Decision | Choice | Rationale |
|---|---|---|
| Pi OS | Raspberry Pi OS Lite 64-bit | Headless, 64-bit for Docker, official Pi 5 kernel |
| Pi database | PostgreSQL 17 | Robust, matches cloud schema |
| ORM | SQLModel | Combines Pydantic + SQLAlchemy; one model for DB + API |
| Shared models | greenthumb-models package |
Single source of truth for both Pi and cloud |
| Cloud API | FastAPI (Python) | Fast async, auto OpenAPI, shares SQLModel models |
| Cloud DB | Self-hosted PostgreSQL 17 + TimescaleDB | Full control of the data, hypertables for time-series, no vendor dependency |
| Auth | Java Spring Boot (JWT) | Isolated, battle-tested JWT library (jjwt) |
| Gateway | Spring Cloud Gateway | Declarative path routing; one entry point for the backend |
| Frontends | React + Vite + Tailwind v3 (dashboards also use TanStack Query v5) | Modern, type-safe, reactive |
| Photo storage | Cloudflare R2 | S3-compatible, no egress fees, private bucket |
| Remote access | Self-hosted WireGuard | Encrypted hub-and-spoke VPN, no public ports, no third-party coordination server |
| OTA (images) | Watchtower | Image updates (automatic for dashboards, manual promotion for control services) |
GreenthumbOS (Planned)¶
A ready-to-flash Raspberry Pi OS image (GreenthumbOS) is planned.