TAEDS Architecture & Design
Reference Documentation
TAEDS is the Kenya Police traffic accident & enforcement information system. Every layer, from the officer's installable phone app in a no-signal valley, through the API that records the case, to the PostgreSQL database that becomes the national statistic, is engineered so a record captured once is complete, tamper evident and true. Thirteen interactive reference diagrams below document how the system is built, secured, synchronised, delivered and recovered — refreshed to match the main branch of the three TAEDS repositories.
Modular monolith
Thirteen strict Laravel modules in one deployable. Tight borders, single transaction per case, easy operations, not a ball of mud.
PostgreSQL is the truth
One source of truth with PostGIS spatial power. Search, cache and object store are derived, never authoritative.
Offline first by design
Field capture runs on a durable on-device outbox. No signal, no lost record, and replay never duplicates.
Secure by default
Backend-enforced RBAC and data scope, segregation of duties, SHA-256 evidence and an append only audit trail.
Observable by default
Metrics, logs and alerts run on our own infrastructure, so we see problems before users report them and can prove recovery targets.
Complete Platform Architecture
The full request path, from the officer's PWA device through the TLS gateway, the API and application modules, down to the data services and physical infrastructure that run them.
Client layer
- Field officers use an installable PWA (React 19 + TanStack Start) that works offline.
- Command, analytics and administration use the same app in a desktop browser.
- The PWA renders server side (Nitro node) in production behind the same gateway.
Edge & application
- An nginx gateway terminates TLS, proxies
/apiand/sanctumand fronts the PWA. - Laravel (PHP 8.5) exposes the REST API and owns every business rule.
- Thirteen modules share one codebase with enforced boundaries.
Data services
- PostgreSQL 18 + PostGIS 3.6: authoritative store, relational + spatial.
- Redis 7: sessions, cache and the queue broker.
- PostgreSQL FTS: full text search built into the source of truth; MinIO/S3: evidence objects.
- Workers (Redis
queue:work+ scheduler; Horizon bundled) drain the outbox.
Backend Architecture & the Request Path
One deployable Laravel application holding thirteen modules with strict borders, and the single disciplined route every mutation takes through the system.
Module ownership
- Each module owns its tables, Actions, API surface and tests.
- Cross-module access happens only through public contracts, never internals.
- Architecture tests fail CI if a module reaches into a neighbour's internals.
Why a modular monolith
- Whole case transactions are easy: no distributed saga for one accident file.
- One build, one deploy, simple operations and rollbacks.
- Module discipline keeps it clean as volume grows.
Controllers are thin
Controller → Form Request → Authorization → Action → Domain → DB → Outbox → Resource.- Side effects exit through the durable outbox, never inside the request tail.
- Domain code never imports HTTP, cloud SDKs or browser APIs.
| Module | Owns (tables / responsibilities) | Primary consumers |
|---|---|---|
| Accidents | accident, location, road conditions, vehicles, drivers, casualties, witnesses, status history, initial/situation/crime reports, inquest, diary, conclusion | Officers, investigators |
| Enforcement | offences, citations, inspections, verifications, court cases, bonds, cash bail | Enforcement officers |
| Evidence | evidence, chain_of_custody, shared links, medical/parade sub forms | All case roles |
| Reporting | reports, schedules, correspondence, sketches, analytics views | Command, analysts |
| Workflow | transitions, comments, task queue | Investigators, commanders |
| GIS | map tiles proxy, blackspots, boundaries, spatial queries | All roles |
| Integrations | NTSA / IPRS / payment circuit state, outbound calls | Backend workers |
| Administration / Auth / Audit / Sync / Notifications / Analytics | users, roles, permissions, stations, ranks, regions/counties/sub-counties, traffic bases, officer transfers, onboarding requests, login_attempts, audit_logs, outbox, sync_cursors, notifications, analytics snapshots / forecast data | All Roles |
How the thirteen modules relate
The rule in one sentence
- No module reaches into another module's internal tables or classes.
- Dependencies point toward the platform core: authentication, administration, audit, notifications, synchronisation and integrations.
- Domain modules (accidents, evidence, enforcement, GIS) relate to each other only through public contracts, read endpoints or events, never through internal code.
Events keep modules decoupled
- One module publishes a fact through the outbox (for example an accident was approved); the modules that care subscribe and react.
- There are no synchronous calls from one module into another over the network or the class loader.
- Reporting and analytics read across several domains through scoped read models, so a live dashboard never depends on another module's internals.
| Module | Relies on (and why) | What it offers to others |
|---|---|---|
| Authentication | Administration (roles and permissions for the session user) | A verified session and user context that every module's Actions use |
| Administration | Standalone owner of stations, users, roles and reference data | Master data and role checks used across the whole system |
| Accidents | Administration (stations, users, reference data) | The central case record that the file, workflow and analytics build on |
| Evidence | Accidents (a case is the anchor), Administration | Fingerprinted, chain of custody verified exhibits attached to a case |
| Enforcement | Administration (stations, users, offence catalogue) | Citations, bonds and court records that relate to a case when relevant |
| GIS | Accidents (spatial positions), Administration (boundaries) | Maps, black spots and spatial analysis over case data |
| Workflow | Accidents and Enforcement state (approval progress) | The approval and task lifecycle shared by case files |
| Reporting | Accidents, Evidence, Enforcement (read side) | Reports, correspondence and statutory outputs |
| Analytics | Accidents, Enforcement (read side) | Dashboards and statistics consumed by command and headquarters |
| Synchronisation | Platform level, no domain dependency | Outbox, idempotency and sync support that field modules use |
| Notifications | Platform level, no domain dependency | In app and email notifications triggered by events |
| Audit | Platform level, no domain dependency | An append only action trail written by every module |
| Integrations | Platform level, no domain dependency | Resilient outbound calls to NTSA, IPRS, payments and messaging |
Database & Data Flow
Structured records land in PostgreSQL, which also hosts full text search (tsvector), while evidence objects land in MinIO and Redis serves cache. Every store is backed up and archived automatically.
Schema by module
- 96 tables today, every one owned by exactly one of the thirteen modules; ownership is enforced by tests, not convention.
- Location hierarchy (region → county → sub-county), traffic bases, officer ranks/transfers and onboarding live under Administration.
- Human case numbers
TAEDS-ACC-2026-000123; ULIDs for internal keys. - Migrate expand/contract; destructive changes require approval.
Spatial (PostGIS)
- Accident point geometry, road network, county/station boundaries.
- Black-spot clustering and heat-mapping run against geometry.
- GIS basemap tiles are proxied server side (credentials never reach devices).
Evidence objects
- Files stored in MinIO (S3); rows in Postgres reference them by SHA-256.
- Metadata, hashes and custody live in the database, never only in the bucket.
- Backups cover both the database and the object store.
Frontend Architecture & Trust Boundaries
One React PWA serves the field device and the office. Server state, offline state and auth are deliberately separated, and every API payload is validated at the trust boundary.
Stack
- React 19 · TypeScript strict (incl.
noUncheckedIndexedAccess) · TanStack Start/Router/Query · Vite 8 · Tailwind 4 + shadcn/ui. - react-hook-form + Zod for typed forms; Recharts for dashboards; MapLibre GL for maps (OpenFreeMap tiles, keyless).
- PWA via Workbox (
vite-plugin-pwa): installable and offline-capable. - API client is generated by Orval from the backend OpenAPI contract into
shared/api/generated.
State strategy
- TanStack Query: server state cache of API truth.
- Dexie/IndexedDB: durable offline working copy + outbox.
- UI state stays in React; auth has a dedicated boundary.
Rules that hold
- Never hand-edit the generated OpenAPI client (
shared/api/generated) or the route tree. - No raw
fetchin components: one central HTTP client (lib/taeds/http-client.ts). - Every screen models all states: loading · empty · offline · syncing · error · conflict.
Offline First & the Outbox
Records are captured on the device first. The outbox replays them durably and idempotently once a connection returns: the server is always the source of truth.
Capture first
- Every field autosaves to IndexedDB as a local draft.
- A visible status shows
ONLINE / OFFLINE / SYNCING / SYNC ERROR. - The officer never waits on a signal to do their job.
Replay, don't guess
- Local mutations carry idempotency keys.
- The server writes the change and its outbox event in one transaction.
- Horizon / queue workers fan out to search, notifications and integrations.
Guarantees (tested)
- Durable: a crash never loses a queued draft.
- Idempotent: replaying never duplicates a record.
- Deterministic conflicts: server resolution wins, always.
Security & Role Based Access
Layered controls from the network edge to the record, and a role matrix with data scope enforced by the backend on every action.
Backend is the lock
- Every sensitive Action checks permission and data scope.
- Self/station scope closes off accidental or malicious cross record access.
- Audit + status history written in the same transaction as the change.
Roles & scopes
- Officers = self · investigators/commanders = station · county/regional command = county/region · HQ/DIG/analyst/ICT = national.
- Ten canonical roles; the RBAC matrix is one tested source of truth (seeded in Administration and locked by an automated RBAC conformance suite).
- The PWA mirrors the same matrix (traffic_officer … system_administrator) for UX only; the backend stays the real boundary.
- Segregation of duties: you cannot approve a case you recorded.
Hygiene
- Sanctum sessions + CSRF; persisted sessions are verified against
/auth/meand are revocable from/auth/sessions. - Login throttling tracks failed attempts (
login_attempts) and rate-limits the password endpoints. - No secrets in code or env files; TLS in transit; no demo account fallback in production.
| Role | Backend code | Data scope | Highlights |
|---|---|---|---|
| Traffic officer | tofficer | self | capture, complete, submit |
| Enforcement officer | enforcement_officer | self | citations, money, court referrals |
| Base investigator | investigator | station | case files, statements, sketches, CRF |
| Base commander | commander | station | review & approve, reports, users view |
| County commander | county_commander | county | review & approve, reports, users view |
| Regional commander | rcommander | region | review & approve, reports, users view |
| HQ administrator | hqadmin | national | approvals, master data, audit |
| Deputy Inspector General | dig | national | oversight, senior approvals |
| Data analyst | analyst | national (read) | analytics, exports, forecasts |
| System administrator | ictadmin | national | settings, security, users |
Evidence & Chain of Custody
From a photo taken at the scene to an archived exhibit: every byte is fingerprinted with SHA-256 and every touch is appended to an un-editable custody chain.
Capture & hash
- Client computes SHA-256 of the real bytes; server re-hashes and compares on verify.
- Status lifecycle: pending → uploaded → verified → approved.
- Storage keys derive from hashes: corruption is detectable.
Chain of custody
- Append-only events: captured · reviewed · downloaded · transferred · approved.
- Each event records who, when, from where (IP) and any details.
- Nothing is silently overwritten: change is a new event.
Governance
- Public share links carry random capability tokens and expiry.
- Medical (P3), parade and specimen sub forms follow the same integrity rules.
- Defined by a dedicated design record and enforced with security tests.
Accident Case Workflow & State Machine
One accident travels through a legal state machine from the officer's draft to a closed national record, with returns, escalations and approvals guarded by roles.
Officer track
- Draft → submitted on completion (owner-only); role-gated commands (submit/assign/etc.).
- Completeness meter before submit: no half-finished cases.
- Returned cases come back with the commander's comment.
Investigator track
- Cases act as one state machine: draft → submitted → assigned → under_review → approved → closed, with rejected, returned and escalated paths.
- Diary, statements, sketches, conclusion, CRF; evidence validated and approved into the file.
- Conclusion is submitted, then approved or returned for correction.
Command & beyond
- Approve / return / escalate: each with comment and audit.
- Approved cases feed dashboards, exports and national statistics.
- Status transitions are legal only via backend Actions.
Integrations & Resilience
NTSA, IPRS, payments and messaging integrate through one failure-tolerant contract. No external provider is ever the source of truth.
Connected providers
- NTSA: vehicle & driver verification.
- IPRS: identity screening.
- eCitizen / payments and email/SMS: notification & court outputs.
Failure contract
- Timeouts and exponential backoff; circuit breakers stop hammering dead providers.
- Feature flags and cached fallbacks keep the app usable.
- Every integration call is audited.
GIS tiles
- Basemap tiles are proxied by the backend so provider keys never reach a device.
- Public CARTO fallback keeps maps working with no provider configured.
CI / CD & Delivery Pipeline
Every change travels the same automated path from commit to production, with contract-first APIs, per-repository quality gates and image-based delivery.
Three repositories, one contract
- TAEDS-Platform: docs, constitution, decisions.
- TAEDS-Backend: Laravel + OpenAPI contract.
- TAEDS-Frontend: React PWA; client generated from the contract.
Quality gates
- Backend CI (
backend.yml): migrate & seed against a fresh Postgres service, thenpesttests,phpstanandpint. - Frontend CI (
frontend.yml):typecheck · lint · format:check · vitestthen production build. - Playwright E2E specs live in the repo but run locally, not in CI, until the runner is provisioned.
- Every gate must pass before a change can ship.
Delivery
- Pushing to
main(or manualworkflow_dispatch) publishesghcr.io/odhiamboatieno/taeds-backendandtaeds-frontendas:latest+:shaimages. - Production stack (
docker-compose.prod.yml) pulls those images behind an nginx gateway; dev stack (docker-compose.yml) hot-reloads via bind mounts. - Environment-specific config comes from env/secret management, never the repo.
Observability, Operations & Disaster Recovery
Well behaved containers, queue workers, healthchecks and a backup ladder, with restore paths tested on a schedule.
What runs
- Dev/prod compose:
gateway(nginx) ·backend(PHP-FPM + nginx :8080) ·backend-worker(queue:work) ·backend-scheduler(schedule:work) ·frontend(Nitro node :3000) ·pgsql(PostGIS) ·redis·minio+minio-init·mailpit(dev / prodmailprofile). - Planned, not yet deployed: Prometheus (metrics), Grafana (dashboards), Alertmanager (alerts), Loki (log shipping and storage).
- Queue workers drain the outbox; scheduler runs housekeeping (backups, aggregation, sync reconciliation).
Backup ladder
- Daily (and periodic weekly/monthly) PostgreSQL dumps + volume snapshots; evidence objects replicate to an offsite/cold tier (MinIO).
- Proposed recovery targets (under development): recovery point objective 24 hours, recovery time objective 4 hours.
- A backup freshness alert pages operations if the latest dump is older than the objective; restore drills follow the runbook (
docs/runbooks) on a fixed schedule.
Observability
- Planned stack: Prometheus scrapes a metrics endpoint and exporters; Grafana renders service and business dashboards.
- Alertmanager turns rule breaches into pages with severity and an escalation path (pending).
- Requests already carry a
request_idend to end; structured logs are shipped today and will flow to Loki for correlation once monitoring lands. - Full details and proposed service targets in § 16 Observability and monitoring.
Design Decisions: why each technology was chosen
Every significant choice below was made after building and running the pilot, and is explained the way I would explain it to you in person. Each decision balances capability, running cost, security and long term supportability for the National Police Service; where we consciously gave something up, we say so and explain the benefit we bought instead.
Backend, data & infrastructure decisions
| Decision | Chosen | Alternatives weighed | Why this won | Revisit when… |
|---|---|---|---|---|
| Language & framework | PHP 8.5 + Laravel 13 | Node/NestJS · Java/Spring · Go · Python/Django · C#/.NET | Fast to deliver and safe by default. Laravel gives first-class ORM, migrations, queues and role/permission tooling out of the box, and static analysis catches whole classes of bugs before deployment, which matters when an officer in the field cannot wait for a hotfix. | Only if the platform later grows into polyglot services with a different runtime need. |
| Application topology | Modular monolith | Microservices · serverless · classic single app | One accident = one transaction across many tables: no distributed saga. One build/deploy keeps ops trivial; strict module boundaries give service like discipline without the operational tax. Serverless adds vendor lock & cold-start pain for a government data platform. | If any single module needs independent scaling or a separate release train. |
| Database engine | PostgreSQL 18 + PostGIS | MySQL/MariaDB · MS SQL · Oracle · MongoDB | ACID reliability + rich constraints; excellent spatial support (PostGIS) for crash geometry; JSONB for flexible form payloads; open source with mature ops. MongoDB lacks the relational integrity a case file demands; MySQL spatial is weaker. | Not planned. PostgreSQL stays the source of truth; we extend it instead of replacing it. |
| Primary keys | ULIDs (sortable) internally; human numbers externally | UUIDv4 · auto-increment int · natural keys | ULIDs are globally unique for offline first sync, sortable by time (useful for cursors), and unguessable: no ID enumeration. Auto-increment leaks volume and breaks offline creation. | n/a |
| Authentication | Laravel Sanctum cookie sessions + CSRF | Pure stateless JWT · Passport/OAuth2 · API keys | Cookies are revocable server side instantly and avoid long-lived bearer tokens on devices; same-site policy + CSRF protect browser flows. Sessions verified against the server before any local session is trusted. | If machine-to-machine partners need OAuth2: add an OAuth bridge, keep sessions for humans. |
| Cache, sessions, queues | Redis (+ Laravel Horizon) | RabbitMQ · Kafka · Beanstalkd · in-memory | One well understood store covers cache, sessions and a reliable queue; Horizon gives a dashboard and supervised workers. Kafka is overkill at this volume. | If event volume/ordering demands Kafka. |
| Search | PostgreSQL FTS (tsvector + GIN) | Elasticsearch/OpenSearch · Postgres trigram · hosted search | Search lives inside the source of truth: no second index to drift or operate, and an officer's cross record search is served by ranked tsvector indexes (with trigram for prefix/typo tolerance). Elasticsearch adds a cluster for marginal gain at this stage. | If cross field analytics search outgrows Postgres, bolt on a dedicated engine: data model is already engine-agnostic. |
| Object storage | MinIO (S3-compatible, pgsty/minio fork image) | AWS S3 primary · local disk · Azure Blob | S3 API is the de-facto standard (swap to any provider later); self hosted keeps evidence inside police infrastructure; local disk lacks durability & replication story. We run the pgsty/minio fork because upstream minio/minio was archived with an unpatched CVE; the fork continues security fixes. | If a national cloud/MoICT directive mandates a provider: switch driver, not code. |
| Monitoring and alerting | Prometheus + Grafana (+ Promtail + Loki for logs) — stand-up planned | Datadog / New Relic (SaaS) · ELK stack · bespoke scripts | Open source and self hosted, so telemetry never leaves police infrastructure (same rule as the data itself). Prometheus pulls metrics from exporters with a well supported ecosystem; Grafana gives one place for service and business dashboards; Promtail and Loki keep log search lightweight and consistent with the PostgreSQL full text decision. The stack is designed but not yet deployed. | If central log volume or long retention outgrows Loki. |
| Containers & orchestration | Docker + Compose (dev & prod) | Kubernetes · Nomad · bare metal · serverless | Compose is the smallest thing that gives reproducible dev/prod parity on one host today; K8s would add a whole platform team. Container images are portable if we grow into K8s later. | When multi-host scaling / self healing is required. |
| Image registry & delivery | GHCR immutable images + tags | Build-on-server · other registries | One immutable artifact per commit, signed in CI, tagged latest + short sha: makes rollback a tag revert, not a code fix. | n/a |
Frontend & offline decisions
| Decision | Chosen | Alternatives weighed | Why this won | Revisit when… |
|---|---|---|---|---|
| Delivery form | Installable PWA (React) | Native Android/iOS · React Native · Flutter | One codebase for phone and desktop, zero app-store friction for officers, works offline via service worker + IndexedDB. Field devices are mixed Android; PWA covers them all and updates instantly. | If deep hardware (camera pipelines, offline maps at scale) demands native. |
| Frontend framework | React 19 + TanStack Start/Router | Vue/Nuxt · SvelteKit · Angular · Next.js | React is battle tested at national scale with the largest library of proven components, which lowers risk and speeds delivery. TanStack gives type safe routing and rendering without framework lock-in, and file based routes keep a growing app navigable and predictable. | n/a |
| Server state | TanStack Query | Redux Toolkit · SWR · manual fetch | Server cache, retries, invalidation and offline semantics out of the box; Redux adds boilerplate for little gain at this scale. | If complex client state grows beyond forms/UI. |
| Forms & validation | react-hook-form + Zod | Formik + Yup · plain controlled inputs | RHF is fast and low-re-render; Zod gives a single schema shared conceptually with the API contract for runtime validation at trust boundaries. | n/a |
| Maps | MapLibre GL + OpenFreeMap tiles | Leaflet · Google Maps · Mapbox | MapLibre GL is a mature open source WebGL renderer; OpenFreeMap serves free, keyless raster tiles (client configured in lib/taeds/map-style.ts), so crash maps work without API keys or vendor quotas. Leaflet was initially bundled but is unused in production. | If offline vector basemaps at scale are required. |
| Offline store | Dexie (IndexedDB) | localStorage · sql.js/WASM SQLite · PouchDB/CouchDB | IndexedDB is durable and queryable; Dexie is a thin, typed layer. localStorage is synchronous, tiny and loss-prone; SQLite in browser needs WASM shipping; PouchDB drags a sync server we don't need (we own the API). | If we need complex local joins, re-evaluate sql.js. |
| Sync strategy | Durable outbox + idempotency; server authoritative | CouchDB replication · CRDTs · naive replay | CRDTs are clever but foreign to a police DB and risk confusing audit trails. A deterministic outbox with idempotency keys gives "replay never duplicates" semantics with full control and auditable server side events. | n/a |
| API contract & client | OpenAPI + generated client (Orval) | Hand-written TS client · GraphQL · tRPC | OpenAPI is the single contract both sides trust; codegen removes drift. GraphQL shifts complexity and complicates caching/permissions for little benefit here. | n/a |
| Styling | Tailwind 4 + shadcn/ui | CSS Modules · styled-components · hand-rolled design system | Design-token driven, small, accessible primitives; full control without fighting a UI kit. Enforced by design tokens (no raw colours in components). | n/a |
Engineering process decisions
| Decision | Chosen | Why |
|---|---|---|
| Repository layout | Three repositories (platform / backend / frontend) | A clean seam at the API contract: docs & decisions, server code, and client code each release on their own cadence without dragging unrelated diffs. |
| Testing layers | Pest/PHPUnit · Vitest + RTL · Playwright E2E · architecture tests | Each layer protects a different regression: unit for rules, feature/HTTP for API behaviour incl. RBAC boundaries, E2E for the officer journey, architecture tests to keep module boundaries honest. |
| Static analysis | PHPStan (Larastan) + TypeScript strict | Catch whole classes of bugs before they run: essential when a field officer may be offline and can't be patched live. |
| Code style | Pint · ESLint · Prettier | Machines, not meetings, decide formatting; PRs review logic. |
| Change control | Short branches → CI gates → merge to main → publish | Every merge is green before it ships; production is always a known, reproducible commit. |
Trade offs we accepted, and the benefit each buys you
One application, not microservices
- We gave up per-service scaling for a single codebase that is simpler to secure, test, deploy and run.
- The payoff: one accident is processed in one transaction, so a case file can never be left half written across services. If a module ever needs to split out, the API contract already gives us the seam to do it without a rewrite.
Self hosted everywhere
- We gave up the convenience of managed cloud services for the right to keep records and telemetry inside police infrastructure.
- The payoff: no per-user SaaS fees, no foreign data storage, and no vendor that can change terms or disappear. The same open source tools (PostgreSQL, Redis, MinIO, Prometheus, Grafana) are supported across the market if we ever need a second opinion.
PostgreSQL full text search, not a search engine
- We accepted that extreme scale "smart search" is beyond the database, for now.
- The payoff: one less system to run, secure and keep consistent. Ranked, typo tolerant search lives with the data it searches, so search results can never disagree with the source of truth.
Browser sessions, not long lived tokens
- We gave up the statelessness of bearer tokens for server managed sessions.
- The payoff: a session can be revoked the moment an officer reports a lost device, and there is no token file sitting on a phone that a thief can replay.
A web app, not native phone apps
- We gave up a few phone-only device capabilities for one codebase that installs and updates instantly on any phone.
- The payoff: field devices never wait for an app store, and there is no fleet of out-of-date native apps with different behaviour; if a specific hardware feature is ever needed, a native shell can be added later without changing the core.
Convention over configuration
- We accepted framework conventions instead of bespoke plumbing in every area.
- The payoff: predictable, auditable behaviour that a new engineer can understand quickly, and a system the Service is not locked to one individual to maintain.
Data Model: schema, rules, normalization & indexes
How the police case file becomes a relational database: one accident fans out into vehicles, drivers, casualties, witnesses, evidence and history: each owning table designed to stay in third normal form unless there is a tested reason to denormalise.
Keys & identity rules
- Internal keys are ULIDs (globally unique, sortable, unguessable): required for offline creation.
- Human-facing identifiers:
TAEDS-ACC-2026-000123generated server side from a per year sequence. - Every row that is "owned" records
created_by/updated_byfor accountability.
Normalization stance
- Targets 3NF: facts live once; vehicles aren't repeated inside casualty rows.
- Example: accident → vehicles → drivers(1:1 to vehicle); accident → casualties; accident → witnesses.
- Small lookup lists (road surface, weather, lighting, offence) are reference data, referenced by FK, not free text.
Intentional denormalization
- Read-optimised aggregate snapshots (
analytics_snapshots,forecast_data) computed on a schedule from the source of truth. - Counts shown on cards are computed in queries (with eager-loading), not stored and allowed to drift.
- JSONB used only for genuinely flexible payloads (form extras), never for core relations.
Core tables & their purpose
| Module | Table (principal) | Purpose / notable fields |
|---|---|---|
| Accidents | accidents | case_number, status, severity, accident_date_time, station_id, created_by, version, sync_status |
| Accidents | accident_locations / _road_conditions / _weather | road, geometry (PostGIS point), surface/condition, weather/illumination/alcohol |
| Accidents | accident_vehicles → accident_drivers | vehicle & owner data; driver 1:1; licence details |
| Accidents | accident_casualties | class of road user, injury severity (fatal/serious/slight/property damage), safety measures |
| Accidents | accident_witnesses | statement data at the scene |
| Accidents | accident_status_history | append only state changes: from, to, by, at, comment (the on-screen timeline) |
| Forms/files | initial_reports · situation_reports · crime_incident_reports · investigation_diaries · investigation_conclusions · sketches · crf_records … | digitised A to J file sections as structured rows |
| Evidence | evidence | type, status, captured_at, sha256_hash, sha256_verified, storage_key, classification |
| Evidence | evidence_chain_of_custody | append only events (who / when / ip / action / details) |
| Enforcement | offences · traffic_citations · bonds · cash_bail_receipts · court_cases | offence catalogue, citations, money tracks, court flow |
| Administration | users · roles · role_permission · permissions · police_stations · organizations · reference_data | identity, RBAC, geography, lookups |
| Administration | regions · counties · sub_counties · traffic_bases · ranks · officer_transfers · officer_onboarding_requests · login_attempts | location hierarchy (region → county → sub-county → base), officer lifecycle, login throttling |
| Platform | outbox_events | durable side effect queue written in the same transaction as the change |
| Platform | sync_cursors · sync_conflicts | device sync state; deterministic conflict ledger |
| Platform | audit_logs | immutable security & action trail |
Schema rules & integrity constraints
Constraints
- NOT NULL on mandatory fields; CHECK/enum-like constraints for statuses, severities, injury classes.
- Foreign keys everywhere relations exist (no orphaned children); deletions are restricted, not cascaded, for case data.
- Unique indexes: case_number, per year sequence, share tokens, citation references.
Integrity by workflow
- State transitions are legal only through backend Actions, not raw SQL or free-form edits.
- Status history + audit write in the same transaction as the change.
- Evidence rows cannot be overwritten silently; changes are new chain events.
Indexing policy
- PKs/FKs indexed; covering indexes on list filters (status, severity, station, date).
- GiST spatial indexes on geometry; GIN
tsvectorindexes power full text search; GIN trigram indexes add prefix/typo tolerance. - Queries eager-load relations to avoid N+1; lists paginate server side with stable ordering.
Migrations & evolution
- Expand / contract phases; destructive steps require explicit approval.
- Migrations are versioned and run forward only in pipelines (rollback = deploy old app + forward fix).
- Dates stored in UTC; human formatting happens at the edge.
Complete table inventory: all application tables (96)
Accident case file · 11
accidents · accident_casualties · accident_damage_reports · accident_drivers · accident_inquests · accident_locations · accident_road_conditions · accident_status_history · accident_vehicles · accident_weather · accident_witnesses
Accident forms, reports and files · 12
accused_statements · crime_incident_reports · crf_records · initial_reports · investigation_conclusions · investigation_diaries · investigation_reports · official_forms · persons · road_condition_records · situation_reports · sketches
Evidence and integrity · 15
descriptive_forms · drink_drunk_examinations · evidence · evidence_chain_of_custody · exhibit_memos · exhibits · fingerprints · identification_parades · medical_reports · parade_members · parade_results · parade_witnesses · personal_effects · post_mortems · specimens
Enforcement and prosecution · 11
bonds · cash_bail_receipts · charge_sheets · court_cases · driver_verification_requests · enforcement_actions · notices_of_prosecution · notices_to_attend_court · offences · traffic_citations · vehicle_inspections
Administration, identity and RBAC · 22
auth_events · counties · devices · login_attempts · officer_onboarding_requests · officer_transfers · organizations · password_reset_tokens · permissions · personal_access_tokens · police_stations · ranks · reference_data · regions · role_permission · roles · sessions · sub_counties · system_configurations · traffic_bases · user_role · users
GIS and analytics · 5
analytics_snapshots · blackspots · forecast_data · gis_locations · safety_recommendations
Reporting and correspondence · 5
correspondence_memos · export_jobs · report_schedules · reports · scheduled_reports
Workflow, platform plumbing and framework · 15
audit_logs · cache · cache_locks · failed_jobs · integration_circuit_state · integration_requests · job_batches · jobs · notifications · outbox_events · sync_conflicts · sync_cursors · workflow_comments · workflow_instances · workflow_transitions
vehicle_ref. Editing one vehicle's registration fixes every casualty that references it, and there is exactly one place a fact lives.CI / CD in Depth, from commit to production
Continuous integration turns every pull request into a verified build; continuous delivery turns every main merge into a published, deployable image. This section explains the pipeline job by job, and how environments, migrations and rollback actually work.
CI: integration
- Triggers: every pull request, push to
main, and manualworkflow_dispatch. - Backend job (
backend.yml): boots a fresh PostGIS 16 service →composer install→ migrate & seed the full schema →php artisan test(Pest) →phpstan→pint --test. - Frontend job (
frontend.yml):npm ci→typecheck(Strict TypeScript) → ESLint → Prettier format check → Vitest → productionbuild. - Playwright E2E spec files live in the repo but run locally, not in CI, until a E2E runner is provisioned.
- A red gate blocks the merge. CI never lies to you.
CD: delivery
- Publishing runs only on
mainor an explicit manual trigger (workflow_dispatch). - Docker images built from the same commit with BuildKit caches and pushed to GHCR:
ghcr.io/odhiamboatieno/taeds-backendandtaeds-frontend, taggedlatest+ short sha. - Production (
docker-compose.prod.yml) pulls the new image, runs forward migrations, restarts app + workers, then health-checks. - Rollback = point compose at the previous sha tag and redeploy.
- Healthchecks gate restarts (
/health/live); target metrics like outbox age are watched once monitoring lands (§ 16).
Contract-first seam
- The OpenAPI spec is the contract between backend and frontend.
- Frontend regenerates its typed client from the contract; never hand-edits generated code.
- Both sides validate the contract in CI so an API change can't silently break the UI.
Environments & configuration
| Environment | Runs from | Config highlights | Who uses it |
|---|---|---|---|
| Developer | Docker Compose, hot-reload of source | debug on, Mailpit for mail, seeded reference data, pilot scope toggleable | Engineers |
| Staging / demo | Published images (or compose) against staging env | realistic data, production-like behaviour, no debug | Reviewers, demo, training |
| Production | Published images behind an nginx gateway | debug off, real SMTP, TLS, backups on, no demo account fallback | Live users |
Threat Model & Controls
A national police record system holds sensitive personal data and court-relevant evidence. Using the STRIDE lens, each threat class is mapped to the concrete control that mitigates it, and to the test that proves it.
| Threat (STRIDE) | Real-world risk here | Control | Proven by |
|---|---|---|---|
| Spoofing identity | Someone acts as an officer or commander | Sanctum sessions, server side session verification, CSRF protection, role-scoped login | Auth feature tests + session verify flow |
| Tampering | Evidence photo or record altered after capture | SHA-256 of real bytes + append only chain of custody + verify action | Evidence integrity tests |
| Repudiation | Officer denies approving / editing a case | Audit log + status history written atomically with each change | Audit & workflow tests |
| Information disclosure | Officer reads another station's cases | Backend data-scope enforcement (self/station/county/region/national) on every query | RBAC boundary tests (HTTP-level) |
| Denial of service | Abuse of public endpoints / tiles | Rate limits, request timeouts, bounded per_page, caching, healthchecks | Load & validation tests |
| Elevation of privilege | Officer approves their own case; sub-entity edits cross scope | Segregation of duties on approval; scope checks on all sub-entity routes (fixed + regression tested) | RBAC conformance suite |
| Web app abuse | XSS, CSRF, SQL injection, mass assignment | Framework escaping + CSP posture, CSRF tokens, prepared statements/ORM, explicit fillable lists | Framework + OWASP-oriented review |
| Secret leakage | Keys shipped in repo or client bundle | No secrets in code; tile/API keys server side only; CI secret scanning posture | Repo hygiene + env model |
| Data loss | Server dies / disk fails / human error | Postgres dumps + volume snapshots, object replication to cold tier, tested restore drills | DR runbook + scheduled drills |
Detection and alerting
- Metrics and alerts (see § 16) will give us early warning, not just post mortems, once the monitoring stack is stood up.
- Suggested signals: failed login spikes, unexpected approval volume, audit trail tampering attempts, outbox backlog, disk and backup freshness.
- Alert noise is kept low by routing to the right owner at the right severity; monitoring data itself is access controlled like any sensitive record.
Observability and Monitoring
The design targets a self hosted monitoring stack — Prometheus collects metrics, Grafana visualizes them, Promtail ships logs to Loki, and Alertmanager pages the right person when something breaks. It is planned, not yet deployed: today we rely on structured logs, request tracing ids and healthchecks. All numbers below are proposed targets for the active development phase, not guarantees yet.
Collect: Prometheus
- The application exposes a metrics endpoint; Prometheus pulls it on a short interval.
- Exporters add metrics for PostgreSQL, Redis, MinIO, the web server and the host itself.
- Metrics are labelled (route, status, station, device) so dashboards can slice by scope.
Visualize: Grafana
- One dashboard per audience: service health, API latency and errors, queue and outbox depth, sync success by device, storage and backup freshness, evidence store growth.
- Anonymised read access for analysts who need business trends only.
Logs: Promtail + Loki
- Promtail ships container logs with the same
request_idused across services. - Loki stores logs cheaply and pairs with the metrics timeline for root cause analysis.
- Log rotation and retention are set so evidence and audit logs are never truncated silently.
Alert: Alertmanager
- Rules fire at three severities: warning (inform), critical (page), and incident (escalate).
- Routing goes to the right runbook owner, with a fallback channel when no one acknowledges.
- Silencing is time boxed and audited; false alerts are tuned, not deleted.
Example metrics we intend to watch
| Signal | What it tells us | Example rule |
|---|---|---|
| HTTP error rate | Broken requests on the API | Alert if more than 1% of requests fail for 5 minutes |
| Latency | Slow responses for officers | Alert if the 95th percentile is above 1 second for 10 minutes |
| Outbox age | Sync or side effects are stuck | Alert if the oldest outbox event is older than 5 minutes |
| Sync success | Devices failing to reconcile | Alert if successful syncs drop below 99% over an hour |
| Queue depth | Workers falling behind | Alert if queue length keeps growing beyond a set depth |
| Database and disk | Capacity and bloat | Alert on disk above 80%, long running queries, connection saturation |
| Backup freshness | Recovery is not actually possible | Alert if the latest dump is older than the recovery point objective |
Service targets and recovery targets
Service level indicators and objectives
- A service level indicator (SLI) is the measured number; a service level objective (SLO) is the target it must meet.
- Proposed objectives: API availability at least 99.5% per month, 95th percentile latency under 1 second, outbox drained within 5 minutes, sync success at least 99%.
- Objectives are reported honestly on dashboards; missing one starts a review, not a hidden incident.
Recovery point and time
- Recovery point objective (RPO) is how recent data must be after a restore; proposed 24 hours with daily dumps and volume snapshots.
- Recovery time objective (RTO) is how fast service must return; proposed 4 hours for the pilot deployment.
- Restore drills run on a schedule and the measured times are compared against these targets.
Codebase Structure: backend and frontend
How the source is arranged so the two codebases stay navigable, testable and modular as the system grows. Trees below are simplified and representative; a few stable files are shown so you can find your way around immediately.
Backend layout principles
- Shared application code lives in
app; every feature lives in its own folder underModules. - A module owns its routes, controllers, requests, actions, models, migrations and tests, so a feature team touches one folder.
- Business rules sit in
Domain/Actions; controllers stay thin; a module cannot reach into another module's internals. - Migrations, seeders and factories sit beside the code that uses them, versioned with the module.
Frontend layout principles
- One folder per domain under
features, mirroring the API modules, so a screen change and its tests change together. - Generated code (API client, route tree) is separated from hand written code and updated by tooling only.
- Shared types, permissions and the offline store live under
lib; base UI undercomponents/ui; product UI undercomponents/taeds. - Routes are file based and guarded by role, mirroring the backend permissions for good usability.
Backend folder layout (simplified)
backend/
├── app/ shared application layer
│ ├── Models/ shared models, e.g. the User
│ ├── Http/ · Providers/ app kernel and service providers
│ └── Support/ shared helpers (errors, scoping)
├── Modules/ feature modules
│ └── Accidents/ one module shown as the pattern
│ ├── Database/
│ │ ├── Migrations/ schema changes owned here
│ │ ├── Factories/ test factories for this module
│ │ └── Seeders/ reference and demo data
│ ├── Domain/
│ │ ├── Actions/ business actions: CreateAccident, ApproveAccident…
│ │ └── Contracts/ interfaces the domain depends on
│ ├── Http/
│ │ ├── Controllers/ thin request entry points
│ │ ├── Requests/ validation rules per request
│ │ ├── Resources/ response shapes served to the API
│ │ └── Routes/ route definitions for this module
│ ├── Models/ Eloquent models owned by the module
│ ├── Providers/ module service provider
│ └── Tests/ feature tests, boundary tests, architecture tests
├── config/ database/ routes/ resources/ framework configuration
├── tests/ global test entry points
├── composer.json dependencies
└── README.md · AGENTS.md conventions for working in this repo
Frontend folder layout (simplified)
frontend/
├── src/
│ ├── routes/ file based routes; authenticated pages are role guarded
│ ├── features/ one folder per domain, mirroring the API
│ │ └── accidents/ example feature
│ │ ├── components/ step and tab components, e.g. SceneInfoStep, VehiclesStep
│ │ ├── hooks/ form and data logic
│ │ ├── api.ts public hooks that call the generated client
│ │ ├── mapper.ts single place that maps API shapes to app models
│ │ ├── schemas.ts Zod form schemas used by the forms
│ │ ├── submit.ts the online create and submit sequence
│ │ └── *.test.* tests kept next to what they test
│ ├── components/
│ │ ├── ui/ base UI building blocks
│ │ └── taeds/ product components: shell, dashboards, charts, maps
│ ├── lib/
│ │ ├── taeds/ types, permissions, severity, offline database, sync, maps, forms
│ │ │ ├── permissions.ts role/permission map; form-permissions.ts per-form rules
│ │ │ ├── severity.ts severity → colour/label mapping
│ │ │ ├── map-style.ts MapLibre style URL for OpenFreeMap tiles
│ │ │ └── db.ts · sync.ts Dexie offline database + durable outbox sync
│ │ └── utils.ts shared helpers
│ ├── shared/api/ http client + generated clients and schemas (never hand edited)
│ ├── i18n/locales/ en.json · sw.json (English and Kiswahili strings)
│ ├── styles.css design tokens and global styles
│ └── router.tsx · routeTree.gen.ts · server.ts · start.ts app wiring (route tree is generated)
├── e2e/ Playwright journeys (run locally)
├── test/ fixtures/ test setup and shared fixtures
├── orval.config.ts code generation settings from the OpenAPI contract
└── package.json · README.md · AGENTS.md
What lives where
| If you are looking for | Where to look |
|---|---|
| An API route and its controller | Modules/<module>/Http/Routes and Http/Controllers |
| Business rules for an operation | Modules/<module>/Domain/Actions |
| Validation for one endpoint | Modules/<module>/Http/Requests |
| Schema for a table | Modules/<module>/Database/Migrations |
| Models and relations | Modules/<module>/Models |
| A screen, its form and its tests | src/features/<feature> |
| The role and permission map | src/lib/taeds/permissions.ts on the frontend, guarded Actions in the backend |
| Offline storage and sync | src/lib/taeds/db.ts (Dexie database) and src/lib/taeds/sync.ts (outbox sync) on the frontend |
| Map rendering and basemap style | src/lib/taeds/map-style.ts (MapLibre GL + OpenFreeMap URL) and map components under src/components/taeds |
| The API client code | src/shared/api/generated (regenerated, never edited by hand) |
| How one request flows through files | route file → controller → request → Domain/Actions action → model → response resource |
Appendix: Modules, RBAC, Endpoints & References
Self contained cheat-sheet tables: glossary, digitised forms, key endpoints, and a map to the deep-dive chapters of this manual.
Digitised forms (A to J)
- Forms A–I are digitised (B was not supplied); A accident package · C medical & damage · D statements & parade.
- E accused statement · F diary · G prosecution package.
- H evidence/admin · I summary/findings · misc records.
- Each form maps to structured rows: fill / view / approve by role.
Key endpoints (contract-first)
POST /auth/login · GET /auth/me · POST /auth/logout · /auth/sessions · /auth/password/*GET|POST /accidents · GET|PATCH /accidents/{id}with/assign-investigator · /submit · /approve · /reject · /return-for-correction · /escalateand/investigation-conclusion/{action}/cases/{id}/evidence … /upload-session · /verify · /approve · /chain · /share-link/enforcement/*incl. cash-bail/bond forfeit & return ·/sync/pull · /sync/reconcile · /gis/roads · /dashboard/* · /admin/*incl. system health
Glossary
- P41, P3, P81A, P32, P2020A: police/medical form codes digitised.
- OB occurrence book · CR crime register · CRF case record file.
- Case file A to J: the bundle one accident generates.
- Outbox: durable, idempotent side-effect queue.
- RBAC / SoD: role based access · segregation of duties.
- PWA / Dexie: installable app · offline IndexedDB.
- TanStack Start / Router: React SSR framework and type-safe routing.
- Orval: generates the typed API client from the OpenAPI contract.
- MapLibre GL / OpenFreeMap: open source WebGL map renderer and its free, keyless basemap tiles.
- Prometheus / Grafana: metrics collector and dashboard tool (planned).
- Promtail / Loki: log shipper and log store.
- Alertmanager: routes alerts to their owners.
- Exporter: turns component stats into metrics for Prometheus.
- Service level indicator (SLI) / service level objective (SLO): measured number vs target.
- Recovery point objective (RPO) / recovery time objective (RTO): data loss and restore time targets.
| Deep-dive chapter | Where in this manual |
|---|---|
| Why we chose each technology (Laravel, React, PostgreSQL, Redis, MinIO, Docker…) | § 12: Design decisions & rationales |
| Database schema, tables, keys, constraints, normalization and indexing rules | § 13: Data model deep dive |
| Continuous integration & continuous delivery in depth (jobs, gates, images, envs, rollback) | § 14: CI/CD in depth |
| Threat model, defences and how each risk is controlled | § 15: Threat model & controls |
| Prometheus, Grafana, Loki, alerts and service/recovery targets | § 16: Observability & monitoring |
| How the backend and frontend source folders are organised | § 17: Codebase structure |