Public JSON API server for Tesserae widgets, served at
https://api.tesserae.ink.
v1 ships a single endpoint: a channel-aware version check. The structure is laid out so further endpoints (widget catalog, almanac, space events, and later multi-user features) slot in as new routers without a rewrite.
License: AGPL-3.0-or-later.
[ Internet ]
|
v 443/tcp
[ Caddy (systemd) ] auto-TLS, per-IP rate limit, access logs with no IP / no User-Agent
|
| reverse proxy -> 127.0.0.1:8000
v
[ tesserae-api container (docker compose) ]
|-- FastAPI + uvicorn (:8000)
|-- /data volume <- version_cache.json
|-- GeoLite2 mmdb (baked into the image at build time)
|
v postgresql (container network)
[ postgres container ] aggregate stats, named volume, 127.0.0.1:5432 (SSH tunnel only)
[ systemd timer: tesserae-api-poll.timer ] every 15 min
|
v
[ docker exec tesserae-api python -m scripts.poll_github ]
-> refreshes /data/version_cache.json from GitHub
The application never calls GitHub on the request path. A systemd timer polls GitHub every 15
minutes and writes version_cache.json and firmware_cache.json; requests are served entirely
from those caches. If GitHub is unreachable at poll time the previous cache is left untouched, so
the API keeps serving the last known good value.
src/tesserae_api/
main.py FastAPI app factory
config.py settings (paths, repo slug, GitHub token)
routes/version.py GET /version/latest
routes/firmware.py GET /firmware/{kind}/latest
cache/github_releases.py version polling, cache read/write, channel resolution
cache/firmware.py firmware release polling by descriptor asset, resolution
stats/collector.py aggregate writes (SQLAlchemy: SQLite dev, Postgres prod)
stats/geo.py MaxMind GeoLite2 lookup
scripts/
poll_github.py run by the systemd timer (version + firmware)
dump_stats.py maintainer's stats reader
Query parameters:
| Param | Required | Notes |
|---|---|---|
channel |
no | stable (default), main, or edge. |
current |
no | Caller's running version. Stable: SemVer (0.69.18). Main: <ver>+<sha> or bare main. Edge: a pre-release tag. |
install |
no | Client-generated UUID v4, stored opaquely against the stats record. Omit for no cross-day dedup. |
Response headers: Cache-Control: public, max-age=300 and Access-Control-Allow-Origin: *.
stable / edge (edge resolves to the latest prerelease: true release):
{
"channel": "stable",
"current": "0.69.18",
"latest": {
"version": "0.69.19",
"released_at": "2026-07-08T09:15:00Z",
"url": "https://github.com/dmellok/tesserae/releases/tag/v0.69.19",
"notes_headline": "Some fix"
},
"is_current": false,
"versions_behind": 1
}main:
{
"channel": "main",
"current": "0.69.18+abc1234",
"latest": {
"sha": "def5678",
"committed_at": "2026-07-08T10:00:00Z",
"url": "https://github.com/dmellok/tesserae/commit/def5678",
"message_headline": "Commit subject line"
},
"is_current": false,
"commits_behind": 5
}When current is omitted (or unparseable), is_current and versions_behind / commits_behind
are null. If the cache has not been populated yet the endpoint returns 503 with
Cache-Control: no-store and a Retry-After header.
Latest published firmware for a device kind, so a Tesserae server can show an "update available"
badge. The source of truth is the newest published (non-draft, non-prerelease) release of the
firmware repo (dmellok/tesserae-device-firmware) that carries a descriptor-<kind>.json asset;
if the newest release does not cover the kind, older releases are walked until one does. Kinds are
therefore discovered from the release assets, not a config file. Releases are polled and cached to
disk (authenticated with a GitHub token).
Query parameter: current (optional) is the caller's running firmware version, recorded for the
version distribution; it does not change the response. Response header:
Cache-Control: public, max-age=300.
{
"latest": {
"version": "1.6.0",
"released_at": "2026-07-22T21:37:48Z",
"url": "https://github.com/dmellok/tesserae-device-firmware/releases/tag/v1.6.0",
"notes_headline": "Safe Wi-Fi OTA for E1004",
"assets": [
{ "name": "descriptor-seeed_reterminal_e1004.json", "url": "https://.../descriptor-seeed_reterminal_e1004.json", "size": 611, "content_type": "application/json" }
],
"descriptor_url": "https://.../descriptor-seeed_reterminal_e1004.json"
}
}version is the release tag with any leading v stripped (plain semver). descriptor_url is the
matching descriptor asset (for one-click import). An unknown kind, or one no release covers, returns
404 with an empty body (the client treats any non-2xx as "no data"), so 404 is the normal "nothing
to report" signal, never a 500. Adding a new device kind is a release with its
descriptor-<kind>.json asset, no code or config change here.
Telemetry is aggregate only: counts per (day, kind, reported version, coarse country) in
firmware_check_stats. The caller IP is used for the country lookup then discarded; no IP, no
install id, and no per-request row is retained.
Records one widget-install event, called server-to-server by an app backend when a user installs a widget from the marketplace. JSON body:
{ "widget": "spotify", "install": "<uuid>", "version": "0.93.0" }widget is required (a catalog id); install is the app-level install UUID (opaque, dedupe key);
version is the app's Tesserae version (optional). Missing widget returns 400. On success the
endpoint returns 204 No Content with Cache-Control: no-store. The write is best-effort and never
fails the response. Same privacy posture as the other endpoints: coarse geo from the IP, then the IP
is discarded; no IP or User-Agent is stored.
Unique install counts per widget for the Browse UI (Cache-Control: public, max-age=300):
{ "counts": { "spotify": 42, "weather_now": 130 } }Counts are COUNT(DISTINCT install_uuid) grouped by widget (rows with a NULL install UUID are
excluded from the distinct count). ?widget=<id> returns { "widget": "<id>", "count": <int> }.
Called about once a day by an app backend (server-to-server) to report an install's presence and environment. JSON body (all fields optional):
{
"install": "<uuid>", "version": "0.94.2", "channel": "stable",
"os": "linux", "arch": "arm64", "py": "3.12", "deploy": "docker",
"transport": "rest", "devices": "2-3", "companion": "2-3",
"device_kinds": [
{"kind": "pimoroni_inky_4", "fw_version": "1.3.1"},
{"kind": "waveshare_spectra6_13", "fw_version": "2.0.0"}
],
"ha": true
}device_kinds accepts either objects {"kind", "fw_version"} or bare slug strings (in which case
the firmware version is unknown). fw_version is a bare semver (one optional leading v is
tolerated); non-semver values are stored as unknown. The latest firmware per (install, day, kind)
wins on re-ping, and feeds the per-kind firmware map.
Returns 204 No Content with Cache-Control: no-store. Two privacy properties are specific to the
heartbeat:
- Only a day is stored, never a timestamp. Heartbeat timing cannot become a sub-daily presence trace.
- Idempotent per
(install, day). Multiple pings on the same day upsert into one row (the descriptive columns reflect the latest ping), so restarts do not inflate counts.
Enum fields are coerced to their allowed sets rather than rejected: an unrecognised value becomes
other (os/arch/py) or unknown (channel/deploy/transport/devices/companion). devices and
companion share the bucket vocabulary {0, 1, 2-3, 4-9, 10+} (companion is a bucketed count of
paired companion apps). device_kinds keeps only
^[a-z0-9_-]{1,64}$ slugs, deduped and capped at 32. Same geo posture as the other endpoints: the
IP is used for a coarse country/region lookup then discarded. Install-level heartbeat rows are not
exposed publicly; the maintainer reads aggregates via scripts/dump_stats.py.
Interactive OpenAPI docs are served at /docs.
One row per served request. Storage is a SQLAlchemy URL: a local SQLite file in
development, PostgreSQL in production (the postgres service in docker-compose).
Set TESSERAE_DATABASE_URL to switch; unset defaults to sqlite:///data/stats.db.
CREATE TABLE hits (
ts TIMESTAMPTZ NOT NULL, -- DATETIME under SQLite
install_uuid TEXT, -- client-generated UUID, NULL if the client omitted it
country TEXT, -- coarse geo from MaxMind, IP discarded after lookup
region TEXT,
channel TEXT, -- version endpoint: requested channel
kind TEXT, -- firmware endpoint: device kind
current_version TEXT
);Both update-check endpoints record into hits. /version/latest sets channel;
/firmware/{kind}/latest sets kind. Both honour the same optional install UUID contract.
Widget installs use a separate widget_installs table (ts, widget_id, install_uuid, tesserae_version, country, region), written by POST /widgets/install and counted by
GET /widgets/installs. The hits table is untouched by the widget path.
Heartbeats use heartbeats (keyed UNIQUE(day, install_uuid), upserted) and heartbeat_kinds
(UNIQUE(day, install_uuid, kind), with a fw_version column for the reported firmware version per
kind). These store a day (a Date), never a timestamp, and are written by POST /heartbeat. The
hits and widget_installs tables are untouched by the heartbeat path.
Firmware checks (GET /firmware/{kind}/latest) write only to firmware_check_stats, an
aggregate-only counter keyed UNIQUE(day, kind, version, country) with a count column. There is
no per-request row, no IP, and no install id: the endpoint upserts a count and stores a coarse
country only.
What is deliberately not collected, ever:
- No IP addresses. The caller IP is used only for the GeoLite2 lookup and is discarded before the DB write.
- No User-Agent strings.
- Caddy access logs record only method, path, status, size, and duration. No IP, no User-Agent.
install_uuid is generated by the client on first install and stored in its own config, so it is
stable across requests and lets unique installs be deduped across days for retention analysis. If a
user resets their widget config they get a new UUID and count as a new install. That is expected.
Summary reader (talks to whatever TESSERAE_DATABASE_URL points at):
# On the VPS (uses the container's Postgres config):
docker exec tesserae-api python -m scripts.dump_stats
# Or against an explicit URL:
python -m scripts.dump_stats --database-url postgresql+psycopg://tesserae:PW@localhost:5432/tesseraePrints: total unique installs, unique installs by country, version distribution, channel distribution, retention (installs seen within the last 7 / 30 / 90 days), and new installs in the last 7 days (first-seen UUIDs).
Postgres listens only on the VPS loopback and is never exposed publicly. Reach it from your machine
over an SSH tunnel, then point psql or any ODBC/BI tool at localhost:5432:
ssh -N -L 5432:127.0.0.1:5432 deploy@api.tesserae.ink # leave running
psql "host=localhost port=5432 dbname=tesserae user=tesserae"See BOOTSTRAP.md for the ODBC DSN details. The password is in
/opt/tesserae-api/.env on the VPS.
uv sync
uv run ruff format --check .
uv run ruff check .
uv run pytest -q
# Run the server locally (writes to ./data):
uv run uvicorn tesserae_api.main:app --reload
uv run python -m scripts.poll_github # populate ./data/version_cache.jsonSee BOOTSTRAP.md for first-time VPS setup. docker-compose.yml is copied to
/opt/tesserae-api/ and Caddyfile to /etc/caddy/Caddyfile on the server.
Three GitHub Actions workflows:
- ci.yml - on every push / PR:
ruff format --check,ruff check,pytest. - release.yml - on a
v*tag: build the image, bake in a fresh GeoLite2 mmdb, push toghcr.io/dmellok/tesserae-api:<tag>and:latest, SSH-deploy to the VPS, then smoke-testhttps://api.tesserae.ink/version/latest. The workflow fails if the smoke test does not return 200. - geoip-refresh.yml - Sundays 06:00 UTC: rebuild
:latestwith a fresh mmdb and redeploy, keeping GeoIP data current without a code release.
Deploy runs automatically on tag. To require manual approval instead, add a GitHub Environment named
production with a required reviewer and set environment: production on the deploy job in
release.yml.
# On the VPS, as the deploy user:
cd /opt/tesserae-api
docker compose pull && docker compose up -dOr re-run the release.yml deploy job from the GitHub Actions tab. A weekly image refresh also
runs automatically.
Secrets live in the repo's Settings -> Secrets and variables -> Actions:
DEPLOY_SSH_KEY- deploy user's private SSH key. To rotate: generate a new keypair, add the new public key to~deploy/.ssh/authorized_keyson the VPS, update the secret, confirm a deploy succeeds, then remove the old public key from the VPS.DEPLOY_HOST,DEPLOY_USER- VPS host / deploy user.MAXMIND_LICENSE_KEY- MaxMind license key for the GeoLite2 download. To rotate: create a new key in the MaxMind account, update the secret, then revoke the old key. The next release or weekly refresh picks it up.
For the GHCR login the deploy user uses on the VPS, generate a new GitHub PAT with read:packages
and re-run docker login ghcr.io as shown in BOOTSTRAP.md.
The database password is not a GitHub secret; it lives in /opt/tesserae-api/.env on the VPS as
POSTGRES_PASSWORD. To rotate it, update the password in Postgres
(ALTER ROLE tesserae WITH PASSWORD ...), edit the .env, and docker compose up -d to restart
the API with the new value.
The published ghcr.io/dmellok/tesserae-api image is public, so any Tesserae install can pull it
without authentication and docker compose pull works unattended. To run your own copy of the API,
follow BOOTSTRAP.md against your own VPS and point your widgets at your own host.
If you prefer to keep the image private (repo public, image private), the deploy user needs a GHCR login on the VPS. See BOOTSTRAP.md step 6.