-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
A high-level map of how GDX Dispatch fits together. It's a single-tenant, self-hosted app: one instance serves one business.
┌──────────────────────────┐
browser ──HTTPS──► │ app (FastAPI + Vue SPA) │ :8001 → :8000
│ uvicorn gdx_dispatch.main │
└───────┬───────────┬────────┘
│ │
┌────────────▼──┐ ┌────▼──────────┐
│ db (Postgres)│ │ redis │
│ one database │ │ auth / cache │
│ "gdx" │ │ celery broker│
└────────────▲──┘ └────▲──────────┘
│ │
┌─────────────────┴───────────┴───────────────┐
│ celery-high celery-low celery-beat │ background jobs
└──────────────────────────────────────────────┘
(optional) plugin-host ◄── /api/plugins/* proxied from app [see Authoring Plugins]
The Vue 3 SPA is built by Vite and served from the same origin as the API in Docker — there's no separate web container.
-
app— FastAPI (gdx_dispatch.main:app) on uvicorn. Serves the API and the built SPA. On boot its entrypoint runsalembic upgrade headthen a bootstrap that seeds the default tenant + admin (idempotent). -
db— a single PostgreSQL 16 database,gdx. It holds both:-
control-plane tables —
tenants,tenant_module_grants,platform_feature_flags,service_accounts,tenant_settings, … -
data-plane tables — customers, jobs, work-orders, estimates, invoices,
users,
company_module_grants, …
(The control/data split is logical, from the multi-tenant era; single-tenant collapse keeps them in one database. The SaaS-facing control-plane surfaces — platform analytics, status page, developer-portal UI, the bare
/v1public API — were removed outright in v1.38.0; the tables remain as the tenant/settings substrate.) -
control-plane tables —
-
redis— JWT refresh-token families + denylist, idempotency keys, rate limiting, and the Celery broker/result backend. -
celery-high/celery-low— priority worker queues (e.g. user-facing vs batch).celery-beat— the scheduler for periodic jobs. -
plugin-host(optional) — runs third-party plugins, isolated fromapp; the core proxies/api/plugins/*to it. See Authoring Plugins.
TenantMiddleware pins every request to the one tenant identified by
GDX_TENANT_ID. Data rows still carry company_id (so the schema is unchanged
from its multi-tenant origins), but there is no per-tenant DB resolution — the
connection is the tenant boundary.
-
JWT (RS256 preferred, HS256 fallback) issued at
/auth/login; refresh + denylist via Redis. -
Roles —
owner>admin>dispatcher>technician>viewer. -
Module grants — features are gated per tenant by
company_module_grants(require_module(...)), which is also what plugins use.
- SPA calls the API with a Bearer JWT.
- Middleware sets the tenant context, enforces rate limits / idempotency.
- The router resolves auth + role + module gates, then does its work against the
gdxdatabase. - Long-running work is handed to Celery (via Redis) and processed by a worker.
Schema lives in Alembic (gdx_dispatch/migrations) with a squashed baseline. The
app container migrates on boot under a Postgres advisory lock, so exactly one
migrator runs even if several containers start together.