Skip to content

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.com and www.transitpin.com serve the marketing site from /home/transitpin/htdocs/transitpin.com, proxied to a PHP backend on 127.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.com and proxies /api/ to http://127.0.0.1:8900 with the original Host header forwarded. This block answers my.transitpin.com, app.transitpin.com, and every tenant subdomain such as villageexpress.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 on 127.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 in init_db() and the per-feature ensure_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

  1. init_db() creates the core tables (idempotent) and runs _migrate_tenant_columns (backfills tenant_id='demo') and _migrate_route_schema (adds routes.mode, creates route_stops), then seeds the demo and enterprise tenants.
  2. Each feature router's ensure_schema(conn) runs (idempotent). maps is a no-op (no tables).
  3. 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 in register.html, which has no inputs). Carries the tenant theme loader and submits full registrations to POST /api/registrations/full.
  • parent.html - parent portal dashboard.
  • index.html - brand marketing surface; redirects to /signin.html when 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

  1. Parent/operator world (product API). GET/POST/etc on most /api/... endpoints require a Bearer token. get_current_user validates the token against auth2 (http://127.0.0.1:8102/api/latest/auth/whoami) and, on failure, accepts tokens prefixed demo- by returning a hardcoded demo user with role: "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.
  2. Ops world (ops.transitpin.com). The ops console does not use /api/auth/login. Its login.html signs in directly against auth2 (https://auth2-api.itpropartner.com) using the password/sign-in endpoint, then calls /api/latest/users/me with an x-hexclave-access-token header and checks membership in the transitpin-it-staff group. 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:

  1. the tenant_id claim on the authenticated user (from the JWT),
  2. the first URL path segment when it is not api (_slug_from_path),
  3. the X-Tenant-Id request header,
  4. the default demo.

resolve_public_tenant (used by no-auth endpoints such as /api/public/schools), in priority order:

  1. the tenant or slug query parameter,
  2. the X-Tenant-Id request header,
  3. the first label of the Host header, skipping reserved labels and all-numeric labels (_slug_from_host),
  4. the first URL path segment when not api,
  5. 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

  1. Browser loads <slug>.transitpin.com/signin.html (or a path-slug URL).
  2. Client-side resolveTenantSlug() identifies the tenant and loadTenantTheme() fetches GET /api/theme/{slug} to apply CSS variables and the brand name.
  3. The user fills the form (parent, optional second parent, one or more children with a transport-need of morning/afternoon/both, emergency contacts, first pickup date, and five policy checkboxes) and submits to POST /api/registrations/full (no auth).
  4. _persist_full_registration writes one households row, a primary payers row, an optional second payers row, one children row per child (with trip_type mapped to one-way/two-way and rate_weekly set), and a registration invoice for $50.00. It also records first_pickup_date and policy_acknowledged on the household and emails a registration-fee notice via local postfix.
  5. 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/generate creates invoices for the current Monday-Sunday cycle for every household with children, splitting each household total across its payers per billing_allocations (percentage/fixed/per-child). Without Square credentials it writes provider='none', status='draft' rows.
  • generate_due_transportation_invoices generates and emails the FIRST weekly transportation invoice for every household whose first_pickup_date is within 5 days (or past). It is idempotent: any household that already has a transportation invoice is skipped. It runs at startup and daily via the transitpin-billing-due.timer systemd timer through generate_due.py.

6.3 Route and stop lifecycle

  • POST /api/routes inserts a routes row plus one route_stops row per stop (sequence numbered 1..N). routes.stops stores the denormalized count.
  • PUT /api/routes/{id} updates scalar fields and, when stops is present, deletes and re-inserts the route_stops rows and re-syncs routes.stops.
  • GET /api/routes/{id} returns the route merged with its ordered stops via _route_with_stops, which JSON-decodes each stop's riders array.

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-dash user.