TransitPin - Architecture¶
Source of truth: the live deployment on app3 (152.53.241.111), verified 2026-08-15 against the running code and database. This document describes the system as it is actually deployed, not as it was originally specified.
1. Purpose¶
TransitPin is a multi-tenant, white-label student transportation platform built for IT Pro Partner. One codebase and one deployment serve many transportation providers ("tenants"), each of which gets a branded parent signup/login surface, an operator dashboard, and per-tenant data isolation. Parents register their children for a provider's weekly school-bus service and are billed a one-time registration fee plus a recurring weekly transportation fee.
2. Topology¶
Internet
|
[ DNS wildcard ]
|
nginx (app3: 80/443)
/ | \
transitpin.com *.transitpin.com ops.transitpin.com
(marketing) (wildcard) (specific block)
| | |
v v v
PHP/FastCGI static docroot static docroot
proxy :8080 /home/transitpin- /home/transitpin-
dash/htdocs/ dash/htdocs/
my.transitpin.com my.transitpin.com
| ( / -> 302 /ops/ )
| /api/ proxy |
v v
+------------------------------+
| FastAPI (uvicorn) |
| 127.0.0.1:8900 |
| systemd transitpin-api |
| /opt/transitpin-api/ |
+------------------------------+
| | \
v v \
SQLite file auth2 (Hexclave) external APIs
data/ http://127.0.0.1: (TomTom, Open-Meteo,
transitpin.db 8102/api/latest Square)
|
v
local postfix relay
(127.0.0.1:25)
|
v
outbound email
2.1 nginx server blocks (three relevant blocks on app3)¶
transitpin.comandwww.transitpin.comserve the marketing site from/home/transitpin/htdocs/transitpin.com, proxied to a PHP backend on127.0.0.1:8080. This is a separate stack from the product.*.transitpin.com(wildcard) serves the product static docroot at/home/transitpin-dash/htdocs/my.transitpin.comand proxies/api/tohttp://127.0.0.1:8900with the originalHostheader forwarded. This block answersmy.transitpin.com,app.transitpin.com, and every tenant subdomain such asvillageexpress.transitpin.com.ops.transitpin.com(more specific than the wildcard, so it wins) serves the same docroot but redirects/to/ops/and also proxies/api/to port- The ops console lives under the
/ops/subdirectory.
TLS uses the wildcard.transitpin.com certificate for the wildcard and ops
blocks, and the apex transitpin.com certificate for the marketing site.
3. Backend (FastAPI)¶
- Entry point:
main.py, an ASGI app served by uvicorn on127.0.0.1:8900. - systemd unit:
transitpin-api.service(User=root, WorkingDirectory/opt/transitpin-api,ExecStart=.../uvicorn main:app --host 127.0.0.1 --port 8900,Restart=on-failure). - Runtime dependencies (
requirements.txt):fastapi==0.115.6,uvicorn[standard]==0.34.0,python-jose[cryptography]==3.3.0,httpx==0.28.1. - Database: SQLite file
/opt/transitpin-api/data/transitpin.db(see DATA-MODEL.md). Schema creation and idempotent migrations run ininit_db()and the per-featureensure_schema(conn)calls at startup.
3.1 Module layout¶
| File | Role |
|---|---|
main.py |
FastAPI app, CORS, auth dependencies, tenant resolution, legacy CRUD endpoints (health, tenants, theme, auth, registrations, routes, buses, schools, children, stats). |
common.py |
Shared helpers duplicated from main.py so feature routers can import auth/tenant/DB dependencies without importing main.py. |
billing_routes.py |
Household/payer billing, payment-provider abstraction, registration-fee and weekly-invoice generation, SMTP email. |
ops_routes.py |
Assume-identity (impersonation) plus the audit trail. |
maps_routes.py |
TomTom traffic health, Open-Meteo weather proxy, per-route driving conditions. |
generate_due.py |
CLI entry point for the daily due-invoice timer. |
The three feature routers (ops_routes, maps_routes, billing_routes) are
wired with app.include_router(router) and NO prefix, because each declares its
routes with the full /api/... path already.
3.2 Startup sequence¶
init_db()creates the core tables (idempotent) and runs_migrate_tenant_columns(backfillstenant_id='demo') and_migrate_route_schema(addsroutes.mode, createsroute_stops), then seeds thedemoandenterprisetenants.- Each feature router's
ensure_schema(conn)runs (idempotent).mapsis a no-op (no tables). billing_routes.generate_due_transportation_invoices(conn)runs as a best-effort catch-up to generate and email any first weekly transportation invoices that are now due. A failure here is logged and does not prevent startup.
3.3 Node relay (WebSocket GPS + messaging)¶
A standalone Node service, server.js (systemd transitpin-relay.service,
enabled), listens on 127.0.0.1:8210 for WebSocket GPS ingestion and messaging
with JSON persistence. It runs independently of the FastAPI app and was verified
live and enabled on app3 as of 2026-08-15. The shipped fleet map polls
/api/buses over REST; the relay is the real-time WebSocket path for GPS data.
4. Frontend (static HTML)¶
The docroot /home/transitpin-dash/htdocs/my.transitpin.com/ holds static
single-file HTML pages (no build step, no framework). Key files:
signin.html- parent login AND registration form (the registration inputs live here, not inregister.html, which has no inputs). Carries the tenant theme loader and submits full registrations toPOST /api/registrations/full.parent.html- parent portal dashboard.index.html- brand marketing surface; redirects to/signin.htmlwhen a tenant slug is detected.about.html,schools.html,policies.html- marketing/legal pages with the same tenant-subdomain guard.admin.html- operator/admin dashboard (dark shell, route management, fleet map).fleet/dashboard.html,fleet/routes.html,fleet/settings.html,fleet/students.html- fleet operator pages.billing.html- billing console.ops/login.html,ops/index.html- the internal ops (super-admin) console.parents/- legacy "find your provider" search (redirected to/signin.html).
The frontend calls the API through the same origin (/api/...), which nginx
proxies to uvicorn.
5. Authentication and tenant resolution¶
5.1 Two auth worlds¶
- Parent/operator world (product API).
GET/POST/etc on most/api/...endpoints require a Bearer token.get_current_uservalidates the token against auth2 (http://127.0.0.1:8102/api/latest/auth/whoami) and, on failure, accepts tokens prefixeddemo-by returning a hardcoded demo user withrole: "admin". Public endpoints (/api/health,/api/theme/{slug},/api/public/schools,/api/billing/rates,/api/billing/provider-status,/api/registrations/full,/api/auth/login,/api/auth/register,/api/health/tomtom,/api/weather) require no token. - Ops world (ops.transitpin.com). The ops console does not use
/api/auth/login. Itslogin.htmlsigns in directly against auth2 (https://auth2-api.itpropartner.com) using the password/sign-in endpoint, then calls/api/latest/users/mewith anx-hexclave-access-tokenheader and checks membership in thetransitpin-it-staffgroup. Impersonation of a customer tenant uses a separate DB-backed token (see section 5.3).
Known limitation: as of 2026-08-15 the auth2 /auth/whoami endpoint used by
get_current_user returns HTTP 404, so real auth2 tokens are rejected and the
working path for gated endpoints is the demo- token fallback. This is a
documented defect, not intended behavior.
5.2 Tenant resolution (two resolvers, different precedence)¶
get_tenant_id (used by auth-gated endpoints), in priority order:
- the
tenant_idclaim on the authenticated user (from the JWT), - the first URL path segment when it is not
api(_slug_from_path), - the
X-Tenant-Idrequest header, - the default
demo.
resolve_public_tenant (used by no-auth endpoints such as
/api/public/schools), in priority order:
- the
tenantorslugquery parameter, - the
X-Tenant-Idrequest header, - the first label of the
Hostheader, skipping reserved labels and all-numeric labels (_slug_from_host), - the first URL path segment when not
api, - the default
demo.
Reserved subdomain labels (RESERVED_SUBDOMAINS): www, my, app, ops,
webmail. These are never treated as tenant slugs.
5.3 Ops impersonation¶
POST /api/ops/assume-identity (platform_admin only) mints a
secrets.token_urlsafe(32) token stored in impersonation_sessions with a
15-minute expiry. Impersonated endpoints accept that token via the Bearer header
through the get_impersonated_user dependency (NOT get_current_user) and write
every action to audit_log attributing both the real actor and the impersonated
subject.
6. Key data flows¶
6.1 Parent registration¶
- Browser loads
<slug>.transitpin.com/signin.html(or a path-slug URL). - Client-side
resolveTenantSlug()identifies the tenant andloadTenantTheme()fetchesGET /api/theme/{slug}to apply CSS variables and the brand name. - The user fills the form (parent, optional second parent, one or more children
with a
transport-needof morning/afternoon/both, emergency contacts, first pickup date, and five policy checkboxes) and submits toPOST /api/registrations/full(no auth). _persist_full_registrationwrites onehouseholdsrow, a primarypayersrow, an optional secondpayersrow, onechildrenrow per child (withtrip_typemapped to one-way/two-way andrate_weeklyset), and aregistrationinvoice for $50.00. It also recordsfirst_pickup_dateandpolicy_acknowledgedon the household and emails a registration-fee notice via local postfix.- Registration also proxies to auth2 via
POST /api/auth/register(a separate call from the form) for identity creation; that path persists nothing to SQLite.
6.2 Weekly billing¶
POST /api/billing/generatecreates invoices for the current Monday-Sunday cycle for every household with children, splitting each household total across its payers perbilling_allocations(percentage/fixed/per-child). Without Square credentials it writesprovider='none',status='draft'rows.generate_due_transportation_invoicesgenerates and emails the FIRST weekly transportation invoice for every household whosefirst_pickup_dateis within 5 days (or past). It is idempotent: any household that already has atransportationinvoice is skipped. It runs at startup and daily via thetransitpin-billing-due.timersystemd timer throughgenerate_due.py.
6.3 Route and stop lifecycle¶
POST /api/routesinserts aroutesrow plus oneroute_stopsrow per stop (sequence numbered 1..N).routes.stopsstores the denormalized count.PUT /api/routes/{id}updates scalar fields and, whenstopsis present, deletes and re-inserts theroute_stopsrows and re-syncsroutes.stops.GET /api/routes/{id}returns the route merged with its ordered stops via_route_with_stops, which JSON-decodes each stop'sridersarray.
7. External services¶
| Service | Purpose | Credential handling |
|---|---|---|
| auth2 (Hexclave) | identity, login, group membership | server-side base http://127.0.0.1:8102/api/latest; ops frontend uses https://auth2-api.itpropartner.com. No credentials stored in the app; keys are not stored in the repo (see Vaultwarden). |
| TomTom Traffic | flow/traffic tiles, incident data, geocoding | API key read at import from admin.html (maps_routes.py) with a constant fallback; client-exposed by design. Redacted here; reference Vaultwarden for the live value. |
| Open-Meteo | weather (free, keyless) | none |
| Square / Stripe | payment providers | SQUARE_ACCESS_TOKEN / SQUARE_LOCATION_ID / STRIPE_SECRET_KEY from environment; Square implemented, Stripe a stub. No keys currently set, so generation falls back to provider none. |
| local postfix | outbound SMTP (127.0.0.1:25) | smarthost SASL creds held in /etc/postfix; no app-level password needed. |
8. CORS¶
The API allows any origin matching
https?://(localhost|127.0.0.1|my.transitpin.com|transitpin.com|.*.transitpin.com)
with credentials, all methods, all headers.
9. Deployment and operations¶
- SSH:
ssh -i /root/.ssh/itpp-infra root@152.53.241.111. - Restart API:
systemctl restart transitpin-api. - Back up the DB before any schema change:
cp /opt/transitpin-api/data/transitpin.db /opt/transitpin-api/data/transitpin.db.bak-$(date +%s). - Frontend edits deploy to the docroot as the
transitpin-dashuser.