Repository navigation
Architecture
How the application is put together: one FastAPI process, a stack of middleware, ten routers, an in-process response cache, and a pooled connection to PostgreSQL. There is no ORM and no background worker.
| Path | What it holds |
|---|---|
main.py |
App factory. Middleware order, router mounting, OpenAPI security schemes, error handlers. |
dependencies.py |
Everything shared: authentication, connection pool, response cache, SQL loader, Pydantic models, app-name canonicalization, WebPubSub broadcast. |
routers/ |
Endpoint logic, one module per area: health, devices, fleet, events, statistics, admin, settings, api_keys, auth_config, inventory_state. |
sql/ |
Parameterized SQL held in .sql files, loaded by name at startup. |
alembic/versions/ |
Schema migrations for the tables this service owns. |
oidc_auth.py |
Provider-agnostic OIDC bearer validation. |
rate_limit.py |
Two-bucket fixed-window rate limiter. |
etag.py |
Weak ETags and 304 handling on /api/v1/ GETs. |
pagination.py |
Link / X-Total-Count / X-Limit / X-Offset headers. |
metrics.py |
Prometheus registry, middleware, and exposition. |
log_tails.py |
Strips and reselects log tails inside the management module, and classifies log lines by level for the fleet sweep. |
install_status.py |
The one classifier for managed-software items on both platforms: stamps each item's state at ingest, suppresses transient network failures, and computes the dashboard counters. |
scripts/export_openapi.py |
Writes the OpenAPI document to docs/api/openapi.json. |
migrations.py |
Runs Alembic programmatically from the app lifespan. |
Middleware is added in main.py; the last one added is the outermost. A
request passes through them in this order:
-
Request ID — honours an inbound
X-Request-IDor mints a 12-hex-character one, binds it to a contextvar for logging, and echoes it on the response. - Metrics — counts and times every request except the metrics scrape itself, labelled by matched route template rather than raw path so serial numbers cannot explode label cardinality.
- Rate limit — see Security. Returns 429 before the handler runs.
- GZip — compresses responses over 1024 bytes.
-
ETag — buffers successful
/api/v1/GET bodies, hashes them into a weak ETag, and returns an empty 304 whenIf-None-Matchmatches. -
CORS — origins come from
CORS_ORIGINS; the list is empty by default, so cross-origin browser calls are refused until you configure it. Allowed methods areGET,POST,PATCH,DELETEandOPTIONS. -
Route handler — with
verify_authenticationas a dependency on every route except/,/health,/health/live,/health/ready, and/auth/config.
ETag sits inside GZip so the tag is computed over the uncompressed body and a 304 skips compression entirely. Rate limiting sits outside so a 429 still carries a request id.
All ten routers mount under the /api/v1 prefix. The unversioned / root
endpoint lives directly on the app and returns a service banner.
| Router | Area |
|---|---|
health |
Health probes, WebPubSub negotiate, Prometheus scrape |
devices |
Per-device reads |
fleet |
Fleet-wide bulk and analytics reads |
events |
Event list, ingest-failure view, event payload, and the ingest POST |
statistics |
Consolidated dashboard: devices, install statistics, recent events |
admin |
Archive, delete, usage-history maintenance, install reclassification, orphan cleanup, diagnostics |
settings |
Org settings document and inventory key discovery |
api_keys |
Mint, list, and revoke per-client API keys |
auth_config |
Public description of the accepted credentials and OIDC details |
inventory_state |
Asset-inventory state per device: bulk write and read |
Endpoints are ordinary synchronous functions, so Starlette runs them in its
threadpool. Each borrows a connection from a DBUtils PooledDB over the pure
Python pg8000 driver and returns it by calling close().
The pool is built lazily on first use and rebuilt if DATABASE_URL changes.
Sizing defaults to DB_POOL_MIN=2 / DB_POOL_MAX=40 — the maximum is matched
to the threadpool so a full house of workers can each hold one connection.
Every physical connection gets statement_timeout = 120s applied with
autocommit on so it survives the pool's rollback-on-return, plus TCP
keepalives so an idle socket is not silently dropped by a NAT gateway.
Long-running maintenance work uses get_maintenance_db_connection(), an
unpooled connection with a caller-chosen statement timeout (default 900s).
The usage-history baseline reset uses it because table-wide work does not fit
inside the 120s request ceiling, and a session-level SET must not leak back
into the pool.
Queries are either inline parameterized statements or files under sql/,
loaded through load_sql() — which rejects path traversal and resolves
against the sql/ directory — and preloaded into memory at import.
dependencies.py holds a plain in-process dictionary cache keyed by
(namespace, key) with a per-namespace TTL: 15s for events, 30s for most
device and fleet reads, 60–120s for the application analytics endpoints.
The cache is bounded two ways, because most namespaces key on the whole
query string and so have an open-ended key space: expired entries are swept on
every write, and a hard cap (RESPONSE_CACHE_MAX_ENTRIES, default 256) evicts
the oldest entry first.
Admin, settings, and inventory-state writes call invalidate_caches(), which
clears everything.
The ingest path deliberately does not invalidate: devices report
constantly, and wiping on every check-in meant the 30-second aggregates never
survived long enough to serve a second request. Ingested data becomes visible
as caches expire.
/api/v1/dashboard additionally holds a per-key asyncio lock so a cache miss
under load recomputes once rather than once per caller, and runs the
synchronous compute in a threadpool so it cannot stall the event loop.
The lifespan handler runs three things in order:
-
assert_auth_enabled_for_prod()— refuses to boot ifDISABLE_AUTHis set in a production-like configuration. -
run_migrations()— Alembicupgrade head. A migration failure is logged but is not fatal; the readiness probe stays the source of truth for whether the database is usable. - On shutdown, the connection pool is drained.
If EVENTS_CONNECTION is set and the Azure Web PubSub SDK is installed, each
stored event is broadcast to the events hub, and GET /api/v1/negotiate
issues a 60-minute client access token so the dashboard can subscribe. With
neither configured, both paths degrade to a response carrying a null token and
an error string, and the dashboard falls back to polling.
AGPL-3.0 — a commercial license is available. reportmate.app