This is the backend code for the Meshbee project. Implements FastAPI REST API, MQTT handler, Mosquitto broker and PostgreSQL, via Docker Compose.
Other parts of the Meshbee project include:
| Repository | What it is |
|---|---|
| meshbee | Umbrella repo: documentation, architecture and the versioned MQTT/API contract. |
| meshbee-firmware | ESP32 firmware for the sensor and gateway nodes (Meshtastic + MQTT). |
| meshbee-server (this one) | Backend: FastAPI REST API, MQTT handler, Mosquitto broker and PostgreSQL, via Docker Compose. |
| meshbee-app | Mobile app in React Native / Expo β dashboards, charts and push alerts. |
| meshbee-hardware | Hardware design: PCB schematics and 3D-printed enclosures. |
π Documentation: https://fablab-imperia.github.io/meshbee/
π οΈ Built by: Fablab Imperia APS
- Overview
- How they fit together
- Requirements
- Setup
- Configuration
- Development
- Troubleshooting
- License
- Versioning and contributing
The server side of Meshbee: it receives hive readings over MQTT, stores them, and serves them to the mobile app over a REST API.
What it does:
- Ingests readings over MQTT, provisioning unknown nodes and hives on the fly.
- Stores them in PostgreSQL, with the measurement ranges enforced twice β in the application and in the schema.
- Serves a REST API with JWT authentication, per-hive permissions and history endpoints sized for charts.
- Records what the beekeeper did: inspections, treatments, harvests.
- Runs entirely in Docker Compose.
What produces the readings and what consumes them are documented in their own repositories β see How they fit together.
Everything named with a trailing slash is a directory in this repository; the two ends of the chain live in sibling repositories.
ESP32 nodes ββMQTTβββΆ mosquitto/ βββΆ mqtt_handler/ βββ
β
ββββΆ meshbee_core/ βββΆ database/
β
mobile app ββHTTPSβββΆ caddy/ ββββββΆ api/ βββββββββββββ
| In the diagram | What it is | Where it lives |
|---|---|---|
| ESP32 nodes | Sensor and gateway nodes. They publish readings to beehive/<id_nodo>/data. |
meshbee-firmware |
mosquitto/ |
The MQTT broker's configuration and state. Off-the-shelf image, no code of ours. | mosquitto/ |
mqtt_handler/ |
Subscribes to the broker, decodes the payload, stores the reading. | mqtt_handler/ |
caddy/ |
Reverse proxy terminating HTTPS on :8443 in front of the API. Optional. |
Local HTTPS |
api/ |
The FastAPI REST API. The only piece the app talks to. | api/ |
meshbee_core/ |
The shared library both entry points import: schemas, services, and all the SQL. | meshbee_core/ |
database/ |
The PostgreSQL schema the library writes to. | database/ |
| mobile app | Dashboards, charts and alerts. Consumes the REST API. | meshbee-app |
Two directories are not on that path: tests/, one pytest suite
covering all of it, and scripts/, one-shot jobs β seed.py creates the initial
accounts, export_openapi.py and export_mqtt_schema.py regenerate the two contract
artifacts.
The architecture of the whole Meshbee project, this repository included, is documented at https://fablab-imperia.github.io/meshbee/architecture/.
Two facts explain most of the layout.
Two processes, one library. api and mqtt-handler are separate containers with
separate lifecycles β restarting the broker does not touch the REST API, and the API is
not an MQTT client. But they write to the same database, so they must agree on what a
valid reading is and on how to store one. That agreement is meshbee_core: a library,
imported by both, never deployed on its own. tests/integration/test_ingest_parity.py
proves the two paths produce identical rows.
Three layers, one direction. SQL lives in meshbee_core/repository/; decisions live
in meshbee_core/services/, which raise their own errors and know nothing about HTTP;
the entry points validate their input, call one service, and translate the outcome
into a status code or a log line. New business logic goes in a service, new SQL goes in
a repository, and a new route is a thin call into an existing service. SQL appearing
in api/, mqtt_handler/ or scripts/ means it went to the wrong place.
Ports:
| Port | Service | Notes |
|---|---|---|
| 8000 | api |
HTTP. /docs, /redoc, /openapi.json. |
| 8443 | caddy |
HTTPS. Only if you ran make certs. Override with HTTPS_PORT. |
| 1883 | mosquitto |
MQTT. Authentication required. |
| 9001 | mosquitto |
MQTT over WebSockets. Configured but unused. |
| 5432 | postgres |
Published for psql and GUI clients. |
Docker and Docker Compose. Optionally git, and mkcert if you want local HTTPS.
git clone https://github.com/fablab-imperia/meshbee-server.git
cd meshbee-server
cp .env.example .env1. Fill in .env. Every value is a credential and every one must be changed:
POSTGRES_PASSWORD=... # database
JWT_SECRET_KEY=... # token signing β openssl rand -hex 32
MQTT_PASSWORD=... # broker
ADMIN_PASSWORD=... # admin@beehive.local, created on first start
USER_PASSWORD=... # utente@test.local, created on first startADMIN_PASSWORD and USER_PASSWORD are required and at least 8 characters. If one
is missing, the seed service stops with an explicit error rather than creating a
working administrator account with an empty password.
2. Generate the broker password file. The broker runs with allow_anonymous false,
so without this step Mosquitto will not start:
make mqtt-passwdRe-run it whenever you change MQTT_PASSWORD β the file holds a hash, so it does not
follow the variable.
3. (Optional) Enable HTTPS. Requires mkcert; it runs on the host and touches your system trust store:
make certsSkip it and Caddy prints a note and exits 0. The rest of the stack, and HTTP on :8000,
carry on regardless.
4. Start.
docker-compose up -d # or: make start
docker-compose psShortcut:
make setupdoes the.envcopy,make mqtt-passwdand the start, in that order. It does not generate certificates.
5. Check.
- HTTP: http://localhost:8000/docs
- HTTPS: https://localhost:8443/docs (only after
make certs)
curl -s localhost:8000/health | python3 -m json.tool
meshbee-seedcreates the initial accounts and exits. Seeing it asExited (0)is normal β it is a one-shot job, not a crashed service.
On a fresh database you get: the two accounts above, one sample node with two hives, and a handful of readings. Change those passwords before exposing anything.
Everything is in .env. Compose passes the values to the services as environment
variables, which take precedence over the file β the .env file itself is only read
directly when you run a process outside Docker.
| Variable | Used by | Notes |
|---|---|---|
POSTGRES_PASSWORD |
postgres, api, mqtt-handler, seed | Also arrives as DB_PASSWORD. Only applied on a fresh volume. |
JWT_SECRET_KEY |
api | Token signing. Changing it invalidates every issued token. |
MQTT_USER |
mosquitto, mqtt-handler | Default beehive. |
MQTT_PASSWORD |
mosquitto, mqtt-handler | Must match mosquitto/config/passwd. |
ADMIN_PASSWORD |
seed | β₯ 8 characters. |
USER_PASSWORD |
seed | β₯ 8 characters. |
HTTPS_PORT |
caddy | Host port for HTTPS. Default 8443. |
Settings are split by service: CoreSettings in meshbee_core/config.py holds the
database fields, and each entry point subclasses it with its own extras β JWT and CORS
for the API, MQTT_* for the handler, the two initial passwords for the seed. Each
component's README lists its own fields.
Put a new field in the narrowest class that needs it. A required field on
CoreSettings must exist in the environment of every service in
docker-compose.yml, or that service crashes on import.
The api container runs uvicorn with --reload and api/, meshbee_core/ and
tests/ are bind-mounted, so editing a file is enough. The MQTT handler has no hot
reload and must be restarted:
docker-compose logs -f # everything (make logs)
docker-compose logs -f api # make logs-api
docker-compose restart mqtt-handler # make restart-mqtt β after every edit there
docker-compose exec postgres psql -U beehive_user -d beehive_iot # make db-shellTests run in the container, always β config.py builds its Settings at import, so
on the host collection fails:
docker-compose --profile test up -d postgres-test # once per boot
docker-compose exec api pytest # make test
docker-compose exec api pytest -m "not integration" # no database neededSee tests/README.md for the two tiers, the fixtures and where a new
test belongs.
After changing a route, a schema or the payload shape, regenerate the committed contract artifacts:
docker-compose exec api python -m scripts.export_openapi # make openapi
docker-compose exec api python -m scripts.export_mqtt_schema # make mqtt-schema
make contract # both at onceapi/openapi.json and mqtt_handler/mqtt-payload.schema.json are what the umbrella
repo's contract
and the mobile app reference. Nothing regenerates them automatically; pytest fails
while the MQTT schema is stale, but nothing checks openapi.json. A new endpoint also
needs a row in the authorization table in tests/integration/api/test_main_authz.py.
Publish a test reading without any client installed:
set -a; . ./.env; set +a
docker-compose exec -T mosquitto mosquitto_pub \
-h localhost -u "$MQTT_USER" -P "$MQTT_PASSWORD" \
-t beehive/NODE001/data \
-m '{"id_sensore":"SENSOR01","temperatura":34.5,"umidita":65,"peso":42.35}'Backups:
make db-backup # β backups/backup_<timestamp>.sql
make db-restore FILE=backups/backup_20260805_120000.sqlStopping: docker-compose stop pauses; docker-compose down (make clean) removes
the containers and keeps the data; make clean-all runs down -v and destroys
the database.
caddy/ holds a Caddy reverse proxy that terminates TLS on :8443 and forwards to
api:8000, using a certificate issued by mkcert
and trusted by your machine. It exists so the mobile app can be developed against
https:// without certificate warnings.
make certs runs on the host (mkcert installs a local CA into your trust store) and
writes caddy/certs/, which is gitignored. Without those files Caddy prints how to
create them and exits 0, so the stack still comes up.
Mosquitto will not start / restarts in a loop. Almost always the missing password
file β it is gitignored, so a fresh clone never has one. Run make mqtt-passwd, then
docker-compose logs mosquitto. Same if you changed MQTT_PASSWORD and did not
regenerate.
A schema or password change had no effect. init.sql and POSTGRES_PASSWORD are
applied only when the data directory is empty, and postgres_data survives
docker-compose down, rebuilds and restarts. Either docker-compose down -v (which
destroys every reading) or write a database/migrate_*.sql. See
database/README.md.
meshbee-seed shows Exited (0). Normal β it is a one-shot job.
seed exits 1 with "Configurazione non valida". ADMIN_PASSWORD or
USER_PASSWORD is missing or shorter than 8 characters. Fix .env, then
docker-compose up -d seed.
Readings never arrive. In order: is the broker up (docker-compose logs mosquitto);
is the handler connected and authenticated (docker-compose logs -f mqtt-handler); is
the node publishing to beehive/<id>/data and not to a deeper topic β the subscription
is beehive/+/data, which matches exactly one level. Then check the payload against
mqtt_handler/README.md: an out-of-range measurement
is dropped, and logged as Lettura scartata.
Code changes in mqtt_handler/ do nothing. There is no hot reload:
docker-compose restart mqtt-handler.
The whole integration tier fails to connect. postgres-test is behind a compose
profile and is not started by a plain up:
docker-compose --profile test up -d postgres-test.
/health says unhealthy. The API is up but the database is not reachable. The
body carries the error; /health deliberately answers 200 either way, so a monitor must
read the body.
Distributed under AGPL-3.0. See LICENSE.
Releases follow semantic versioning; the tag on the latest release is the one to deploy. Compatibility between the repositories of the project is tracked in the umbrella repo: compatibility matrix.
Contributions are welcome β see the organisation's
CONTRIBUTING.
Commits follow Conventional Commits.
Documentation is bilingual: English is canonical (README.md), Italian is the
translation (README.it.md), and both are kept in step.
Open an issue, or write to Fablab Imperia APS.