GPS Engine Design complete, not built

The concrete build behind Architecture's vision

Architecture named GPS Dashboard's direct, client-side Supabase access as the single biggest platform risk, and set out four vision items: each product owns its data, one identity layer, a narrow API between products, and a batch-fed reporting layer. This page is that vision turned into an actual engineering plan for GPS specifically — the first product to get it — verified against the real repositories, not proposed in the abstract. Full detail on every decision lives in the source plan this page summarizes.

Why GPS first

The exact exposure Architecture flagged, confirmed directly

Reading trackezz_etl/supabase/schema.sql directly (not just GPS Dashboard's code) confirms it's worse than "a public key plus RLS": every RLS policy on every GPS table is to authenticated using (true) — any signed-in dashboard login can read the entire fleet's full position history, every driver mapping, every ride/revenue number, with zero per-user or per-row scoping. Upahar's own repo independently reached the same conclusion first: a migration already revoked anon/authenticated grants on its own tables, and its docstring names gps_dashboard by name as the sibling app still exposed.

Also confirmed, not previously documented: trackezz_etl carries substantial business logic as Postgres functions (day/night metrics, GPS anomaly detection, ride-vs-GPS matching, revenue rollups, plate reconciliation) with real production-incident history — a naive DISTINCT ON view that silently became a 46-second scan at production scale, a manual SQL deploy that silently didn't land, DROP FUNCTION-before-CREATE footguns. None of it is unit-testable outside Postgres. This is the "bad ETL design" half of the problem Architecture's debt inventory didn't fully capture.

Target architecture

One shared RDS instance, separate database per product

Architecture's vision said "not one shared database." This plan collapses Supabase-hardening and the eventual RDS migration into one move, and answers why below.

flowchart TB RDS[("Shared RDS Postgres
ap-south-1, one instance")] UPD[("upahar_db")] EXD[("express_db")] GPD[("gps_db")] RTD[("rentown_db
future")] RDS --- UPD & EXD & GPD & RTD UP["Upahar FastAPI
unchanged"] --> UPD EX["Express FastAPI
unchanged"] --> EXD GE["drivra_engine
new"] --> GPD ETL["GPS ETL Lambda
unchanged, writes only"] --> GPD RT["Rent-to-own
not built"] -.-> RTD DASH["GPS Dashboard
rewired"] -->|JSON API only| GE classDef good fill:#d7ecdf,stroke:#1a7a42,color:#14532d,stroke-width:2px; classDef missing fill:#e5e7eb,stroke:#6b7280,color:#374151,stroke-width:2px; class UP,EX,ETL good; class GE,DASH,RT,RTD missing;

Gray: new work this plan builds. Every product owns its own database and is the only thing that talks to it directly — the "shared" part is infrastructure (one instance, one bill, one thing to patch and back up), never data.

Why RDS now, not "harden Supabase, migrate later": drivra_engine (like Upahar already) talks to Postgres directly via SQLAlchemy/asyncpg, never through Supabase's PostgREST layer — the application code is identical regardless of which of the two hosts Postgres. Hardening Supabase in place would be real security work thrown away days later when Express's already-urgent RDS migration (no managed backups today) happens anyway. RDS is also strictly better: a private-VPC instance has no public REST surface at all, versus Supabase's PostgREST endpoint still existing publicly, just de-authorized.

Migration sequencing

1Express → RDS first: most urgent (no managed backups today), lowest coordination cost, a clean rehearsal of the RDS provisioning work
2GPS → RDS, built fresh: drivra_engine never touches Supabase at all — this is the plan's main build
3Upahar → RDS last: no urgent driver, benefits from two prior migrations' worth of learned experience

The missing fourth vision item: a reporting database

Made simpler specifically because every product now lives on one RDS instance: postgres_fdw foreign tables in a new reporting_db point directly at upahar_db/express_db/gps_db — no bespoke extraction Lambda needed, since it's all one instance. dbt owns the transform step (its core use case: testable, documented, lineage-tracked SQL), triggered by one nightly scheduled job. No Step Functions, no message bus — this is exactly the "right-sized, not enterprise-sized" principle Architecture already committed to. This is the piece that unblocks rent-to-own's credit scoring (below).

The new service

drivra_engine: same proven stack, own repo, own data

Mirrors Upahar's architecture directly — not extended into Upahar's own repo (product autonomy), a fresh service copying what's already proven: FastAPI + SQLAlchemy 2.0 (async) + asyncpg + Alembic, routers → services → ORM, JWT auth, Docker + Caddy + GitHub Actions → ECR → SSM deploy, t4g.small EC2 (one size up from Upahar's t4g.micro, given more concurrent dashboard sessions and larger JSON payloads).

API groupReplacesNotes
positions/fetchLatest, history queriesLatest-position endpoint served from a cache table refreshed every 30–60s — faster than today's on-demand recursive-CTE view, not slower
driver-mapping/set_vehicle_driver, lookup, suggestions, reconciliation409 with a structured error code replaces raw Postgres exception text on conflict
attributes/vehicle type/fuel CRUDUnchanged shape
analytics/6 heavy SQL functionsGeofence coordinates move server-side (single source of truth, no more client-supplied config)
auth/Supabase AuthNew ops_users table, JWT, mirrors Upahar's admin-auth pattern exactly — own audience, own secret

Table ownership going forward: vehicle_positions stays Lambda-written (unchanged ETL), drivra_engine reads only. vehicle_driver_mapping and vehicle_attributes get adopted — drivra_engine becomes the sole writer via idempotent migrations that no-op against the already-migrated data. The existing fleet.*_cache tables and pg_cron jobs are superseded, not reused (a new Python-refreshed writer against the same table would race the old pg_cron one during the build) — they go dead at cutover and can be dropped at leisure.

Business logic migration

Port the SQL, don't rewrite the algorithms

The ask is a new service, not a new algorithm. Recommendation: keep row-level computation as SQL (near-verbatim copies of the existing, already-tuned function bodies) called from Python service functions — the new Python work is the orchestration (params, caching, auth, error shaping), not re-deriving window functions and gaps-and-islands logic that's already been tuned through real incidents once.

RiskFunctionWhy
Highest_vehicle_ride_segmentsTwo documented real mis-plan incidents already; a 5-way classification precedence resolved against a slowly-changing-dimension range-join
Highest_vehicle_day_metrics_liveThree-stage documented tuning saga (97s→44s→20s→2-3s); easy-to-invert gap-exclusion and overnight-boundary rules
MediumMaintenance visits, GPS anomaliesSame gaps-and-islands technique, subtler "is this still ongoing" joins
LowRevenue, reconciliation, matching, lookupsStraightforward joins/branching — set_vehicle_driver (lock row, branch, write) is the one genuinely well-suited to a real Python port

Mitigation regardless: before cutting any single function over, a shadow-diff script calls both the still-live SQL and the new endpoint for the same real vehicles/date ranges and diffs row-for-row across 30–60 days × every vehicle — a concrete gate, not a suggestion. Testing also deviates from Upahar's own convention on purpose: Upahar accepts SQLite-only tests because none of its logic is Postgres-specific; that's not true here, so a small Postgres-backed integration suite runs on every PR.

Security, encryption & access control

Closing the exposure completely, not partially

Encryption

RDS storage encryption at instance creation (can't be retrofitted without a snapshot-restore). TLS everywhere — sslmode=require on every connection, Caddy termination on every API. KYC documents (future): S3 with SSE-KMS, never a public bucket URL.

PII handling

Audit logging as a first-class table from day one. Least-privilege DB roles per product. No PII in logs, ever — IDs only. KYC documents (future): 5-year retention floor per Nepal's Asset Laundering Prevention Act, access restricted to a KYC-reviewer role specifically.

RBAC

Three tiers, applied uniformly — read, write, admin — not a per-module permission matrix. Same shape as Upahar's existing owner/staff dependency pattern, extended rather than reinvented.

Hardened posture

WAF on every public API (rate-limited specifically on login), GuardDuty for account-level anomaly detection, in-app rate limiting from day one (a gap Upahar's own research already flagged), MFA for admin-role accounts, dependency scanning in CI.

Migration path

Nothing live breaks until the last possible step

Every phase is additive and reversible until explicitly noted. The dashboard and ETL Lambda keep working, completely unchanged, through Phase 4.

PhaseWhat happensLive system
0Provision drivra_engine's own scoped Postgres role, RLS policiesUntouched
1Skeleton, read-only positions, auth — deployed to staging onlyUntouched
2Driver-mapping + attributes CRUD parity, concurrency-testedUntouched
3Six analytics functions ported, shadow-verified against real dataUntouched
4Dashboard rewritten, deployed to staging only, QA'd page by page against productionUntouched — last fully-reversible checkpoint
5Cutover: production dashboard points at drivra_engineLive on new backend; same-day rollback available for 1–2 week soak
6Revoke anon/authenticated PostgREST grants — mirrors Upahar's own precedent exactlyThe concrete fix for the flagged critical vulnerability
7Cleanup: old SQL functions/cache tables removed at leisureOptional

Cost

Estimated new infrastructure spend

On-demand ap-south-1 rates, not a quote — confirm via the AWS Pricing Calculator before treating this as a committed budget.

~$115–130/moSingle-AZ RDS (db.t4g.medium) + drivra_engine EC2 + Fargate reporting job + storage
~$165–180/moSame, with Multi-AZ RDS — recommended once all three products depend on the instance

Against this: Supabase's usage-based growth avoided, Express's currently uninsured no-backup risk closed, and one migration instead of two (the "harden now, RDS later" alternative pays for this work twice).

AWS services considered, and deliberately not used
CategoryServices
Already in use, unchangedLambda, EC2, ECR, SSM, IAM/OIDC, EventBridge Scheduler, CloudWatch
New, neededRDS, S3, ECS Fargate (nightly dbt), KMS
Needed later, not nowAthena — only once a cold-storage tier actually exists
Recommended againstGlue (no messy multi-source schema-discovery problem exists here), Redshift/a message bus (explicitly what Architecture's vision says to avoid), API Gateway (Caddy already fills this role), Cognito (would replace already-proven JWT auth for no benefit)

Mobile impact

Checked directly: zero changes needed

Upahar's Flutter driver app (app_rider/) was inspected directly, not assumed: no Supabase dependency anywhere in its source or its package manifest. It talks exclusively to Upahar's own API over a plain HTTP client. Since the database migration is entirely internal to the backend, every installed app is unaffected, with no new release required — the concrete payoff of Upahar already having a real backend in front of its data, which is exactly what this plan gives GPS Dashboard for the first time.

Alignment with Rent-to-Own

Same pattern, not a new decision when it's built

This plan's "product autonomy, proven stack, separate database" pattern is exactly what financing.html already independently specifies for the rent-to-own service: its own repo, own database (a database on the shared RDS instance under this model, rather than a fully separate Supabase project), same FastAPI + SQLAlchemy + Alembic stack. Its credit scoring depends on Upahar's tenure/earnings/streak data — exactly the cross-product signal the new reporting database (above) exists to serve, and explicitly does not depend on GPS telematics data in v1.

Open questions

Flagged, not silently decided