TAEDS Kenya National Police Service
Platform Architecture · Reference Manual · v1.2 ↑ Overview
Architecture Principles

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.

Officers Accident Records Evidence Case Workflow Dashboards Backups Audit Trail Business Data
ⓘStatus of this document. TAEDS is under active development and this manual has been reconciled with the codebase as of mid-September 2026 (Laravel 13 · PHP 8.5 · PostgreSQL 18/PostGIS 3.6 · React 19 + TanStack · Docker Compose, dev and prod). It describes the current architecture and the reasoning behind it; specific tables, endpoints, tool versions and pipeline steps will continue to change as the system matures toward national rollout. Diagrams are conceptual snapshots, not pixel-perfect renderings of the running code. Treat anything here as a living specification: the principles in sections 12 to 17 are the stable part. The Prometheus/Grafana/Loki monitoring stack and the numeric targets in section 16 are planned, not yet deployed and will be tuned when stood up.

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.

Reference Diagram 01 · System Stack

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.

01
Live data path
Derived reads (Redis cache)
Worker / sync path
Runs in our own infrastructure
◆Every client request lands on the same spine: UI → Gateway → Laravel → one PostgreSQL transaction → a durable outbox event. Redis and MinIO only ever serve derived data; full text search runs inside PostgreSQL itself.

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 /api and /sanctum and 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.
Reference Diagram 02 · Modular Backend

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.

02
Request path (mutations)
Module boundary

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.
ModuleOwns (tables / responsibilities)Primary consumers
Accidentsaccident, location, road conditions, vehicles, drivers, casualties, witnesses, status history, initial/situation/crime reports, inquest, diary, conclusionOfficers, investigators
Enforcementoffences, citations, inspections, verifications, court cases, bonds, cash bailEnforcement officers
Evidenceevidence, chain_of_custody, shared links, medical/parade sub formsAll case roles
Reportingreports, schedules, correspondence, sketches, analytics viewsCommand, analysts
Workflowtransitions, comments, task queueInvestigators, commanders
GISmap tiles proxy, blackspots, boundaries, spatial queriesAll roles
IntegrationsNTSA / IPRS / payment circuit state, outbound callsBackend workers
Administration / Auth / Audit / Sync / Notifications / Analyticsusers, 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 dataAll Roles

How the thirteen modules relate

Dependency flow
Layer boundary

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.
ModuleRelies on (and why)What it offers to others
AuthenticationAdministration (roles and permissions for the session user)A verified session and user context that every module's Actions use
AdministrationStandalone owner of stations, users, roles and reference dataMaster data and role checks used across the whole system
AccidentsAdministration (stations, users, reference data)The central case record that the file, workflow and analytics build on
EvidenceAccidents (a case is the anchor), AdministrationFingerprinted, chain of custody verified exhibits attached to a case
EnforcementAdministration (stations, users, offence catalogue)Citations, bonds and court records that relate to a case when relevant
GISAccidents (spatial positions), Administration (boundaries)Maps, black spots and spatial analysis over case data
WorkflowAccidents and Enforcement state (approval progress)The approval and task lifecycle shared by case files
ReportingAccidents, Evidence, Enforcement (read side)Reports, correspondence and statutory outputs
AnalyticsAccidents, Enforcement (read side)Dashboards and statistics consumed by command and headquarters
SynchronisationPlatform level, no domain dependencyOutbox, idempotency and sync support that field modules use
NotificationsPlatform level, no domain dependencyIn app and email notifications triggered by events
AuditPlatform level, no domain dependencyAn append only action trail written by every module
IntegrationsPlatform level, no domain dependencyResilient outbound calls to NTSA, IPRS, payments and messaging
◆What this buys the client. A change in one module (say the offence catalogue) cannot break another module's screens or rules, because they only meet through contracts and events. The platform modules stay small and focused, and the domain modules stay replaceable and testable on their own.
Reference Diagram 03 · Persistence

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.

03
Object & document data
Derived reads (Redis cache)
Backup / archive
Source of truth

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.
Reference Diagram 04 · Frontend

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.

04
Feature modules
State strategy
Generated code (never hand-edited)

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 fetch in components: one central HTTP client (lib/taeds/http-client.ts).
  • Every screen models all states: loading · empty · offline · syncing · error · conflict.
iThe frontend mirrors permissions for UX. The backend remains the real security boundary: hiding a button is convenience, not protection.
Reference Diagram 05 · Synchronisation

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.

05
Offline queue
Replay / sync path
Server 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.
Reference Diagram 06 · Defense in Depth

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.

06
Perimeter
Identity & access
Data at rest

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/me and 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.
RoleBackend codeData scopeHighlights
Traffic officertofficerselfcapture, complete, submit
Enforcement officerenforcement_officerselfcitations, money, court referrals
Base investigatorinvestigatorstationcase files, statements, sketches, CRF
Base commandercommanderstationreview & approve, reports, users view
County commandercounty_commandercountyreview & approve, reports, users view
Regional commanderrcommanderregionreview & approve, reports, users view
HQ administratorhqadminnationalapprovals, master data, audit
Deputy Inspector Generaldignationaloversight, senior approvals
Data analystanalystnational (read)analytics, exports, forecasts
System administratorictadminnationalsettings, security, users
Reference Diagram 07 · Evidence Integrity

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.

07
Upload & verify path
Chain of custody

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.
Reference Diagram 08 · Case Workflow

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.

08
Happy path
Return
Escalate

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.
Reference Diagram 09 · External Integration

Integrations & Resilience

NTSA, IPRS, payments and messaging integrate through one failure-tolerant contract. No external provider is ever the source of truth.

09
Outbound via outbox
External provider

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.
Reference Diagram 10 · Release Process

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.

10
Deployment path
Rollback path (revert tag)

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, then pest tests, phpstan and pint.
  • Frontend CI (frontend.yml): typecheck · lint · format:check · vitest then 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 manual workflow_dispatch) publishes ghcr.io/odhiamboatieno/taeds-backend and taeds-frontend as :latest + :sha images.
  • 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.
Reference Diagram 11 · Operations & Continuity

Observability, Operations & Disaster Recovery

Well behaved containers, queue workers, healthchecks and a backup ladder, with restore paths tested on a schedule.

11
Primary stores
Backup / cold tier

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 / prod mail profile).
  • 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_id end 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.
Reference Manual · Deep Dive §12

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.

12

Backend, data & infrastructure decisions

DecisionChosenAlternatives weighedWhy this wonRevisit when…
Language & frameworkPHP 8.5 + Laravel 13Node/NestJS · Java/Spring · Go · Python/Django · C#/.NETFast 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 topologyModular monolithMicroservices · serverless · classic single appOne 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 enginePostgreSQL 18 + PostGISMySQL/MariaDB · MS SQL · Oracle · MongoDBACID 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 keysULIDs (sortable) internally; human numbers externallyUUIDv4 · auto-increment int · natural keysULIDs 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
AuthenticationLaravel Sanctum cookie sessions + CSRFPure stateless JWT · Passport/OAuth2 · API keysCookies 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, queuesRedis (+ Laravel Horizon)RabbitMQ · Kafka · Beanstalkd · in-memoryOne 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.
SearchPostgreSQL FTS (tsvector + GIN)Elasticsearch/OpenSearch · Postgres trigram · hosted searchSearch 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 storageMinIO (S3-compatible, pgsty/minio fork image)AWS S3 primary · local disk · Azure BlobS3 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 alertingPrometheus + Grafana (+ Promtail + Loki for logs) — stand-up plannedDatadog / New Relic (SaaS) · ELK stack · bespoke scriptsOpen 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 & orchestrationDocker + Compose (dev & prod)Kubernetes · Nomad · bare metal · serverlessCompose 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 & deliveryGHCR immutable images + tagsBuild-on-server · other registriesOne 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

DecisionChosenAlternatives weighedWhy this wonRevisit when…
Delivery formInstallable PWA (React)Native Android/iOS · React Native · FlutterOne 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 frameworkReact 19 + TanStack Start/RouterVue/Nuxt · SvelteKit · Angular · Next.jsReact 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 stateTanStack QueryRedux Toolkit · SWR · manual fetchServer 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 & validationreact-hook-form + ZodFormik + Yup · plain controlled inputsRHF 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
MapsMapLibre GL + OpenFreeMap tilesLeaflet · Google Maps · MapboxMapLibre 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 storeDexie (IndexedDB)localStorage · sql.js/WASM SQLite · PouchDB/CouchDBIndexedDB 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 strategyDurable outbox + idempotency; server authoritativeCouchDB replication · CRDTs · naive replayCRDTs 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 & clientOpenAPI + generated client (Orval)Hand-written TS client · GraphQL · tRPCOpenAPI is the single contract both sides trust; codegen removes drift. GraphQL shifts complexity and complicates caching/permissions for little benefit here.n/a
StylingTailwind 4 + shadcn/uiCSS Modules · styled-components · hand-rolled design systemDesign-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

DecisionChosenWhy
Repository layoutThree 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 layersPest/PHPUnit · Vitest + RTL · Playwright E2E · architecture testsEach 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 analysisPHPStan (Larastan) + TypeScript strictCatch whole classes of bugs before they run: essential when a field officer may be offline and can't be patched live.
Code stylePint · ESLint · PrettierMachines, not meetings, decide formatting; PRs review logic.
Change controlShort branches → CI gates → merge to main → publishEvery 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.
◆Net effect: a dependable, well supported stack the Service can operate with confidence for years. The genuinely hard engineering is reserved for the parts that create the value: offline capture, evidence integrity and role based access, not for exotic tooling.
Reference Manual · Deep Dive §13

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.

13
File relationship (one accident owns many)
Parent reference (many to one)
Entity box: name, primary key, foreign keys

Keys & identity rules

  • Internal keys are ULIDs (globally unique, sortable, unguessable): required for offline creation.
  • Human-facing identifiers: TAEDS-ACC-2026-000123 generated server side from a per year sequence.
  • Every row that is "owned" records created_by / updated_by for 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

ModuleTable (principal)Purpose / notable fields
Accidentsaccidentscase_number, status, severity, accident_date_time, station_id, created_by, version, sync_status
Accidentsaccident_locations / _road_conditions / _weatherroad, geometry (PostGIS point), surface/condition, weather/illumination/alcohol
Accidentsaccident_vehicles → accident_driversvehicle & owner data; driver 1:1; licence details
Accidentsaccident_casualtiesclass of road user, injury severity (fatal/serious/slight/property damage), safety measures
Accidentsaccident_witnessesstatement data at the scene
Accidentsaccident_status_historyappend only state changes: from, to, by, at, comment (the on-screen timeline)
Forms/filesinitial_reports · situation_reports · crime_incident_reports · investigation_diaries · investigation_conclusions · sketches · crf_records …digitised A to J file sections as structured rows
Evidenceevidencetype, status, captured_at, sha256_hash, sha256_verified, storage_key, classification
Evidenceevidence_chain_of_custodyappend only events (who / when / ip / action / details)
Enforcementoffences · traffic_citations · bonds · cash_bail_receipts · court_casesoffence catalogue, citations, money tracks, court flow
Administrationusers · roles · role_permission · permissions · police_stations · organizations · reference_dataidentity, RBAC, geography, lookups
Administrationregions · counties · sub_counties · traffic_bases · ranks · officer_transfers · officer_onboarding_requests · login_attemptslocation hierarchy (region → county → sub-county → base), officer lifecycle, login throttling
Platformoutbox_eventsdurable side effect queue written in the same transaction as the change
Platformsync_cursors · sync_conflictsdevice sync state; deterministic conflict ledger
Platformaudit_logsimmutable 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 tsvector indexes 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

◆Worked example of 3NF: a casualty is not a column on the accident row, and its vehicle is not copied into the casualty row: the casualty stores a vehicle_ref. Editing one vehicle's registration fixes every casualty that references it, and there is exactly one place a fact lives.
Reference Manual · Deep Dive §14

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.

14

CI: integration

  • Triggers: every pull request, push to main, and manual workflow_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 → production build.
  • 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 main or an explicit manual trigger (workflow_dispatch).
  • Docker images built from the same commit with BuildKit caches and pushed to GHCR: ghcr.io/odhiamboatieno/taeds-backend and taeds-frontend, tagged latest + 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

EnvironmentRuns fromConfig highlightsWho uses it
DeveloperDocker Compose, hot-reload of sourcedebug on, Mailpit for mail, seeded reference data, pilot scope toggleableEngineers
Staging / demoPublished images (or compose) against staging envrealistic data, production-like behaviour, no debugReviewers, demo, training
ProductionPublished images behind an nginx gatewaydebug off, real SMTP, TLS, backups on, no demo account fallbackLive users
iRules that keep delivery safe: secrets live in the environment/secret store, never in images or the repo · migrations run forward only before the app rollout · every image is immutable and tagged · healthchecks gate restarts · no demo fallback path in production.
Reference Manual · Deep Dive §15

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.

15
Threat (STRIDE)Real-world risk hereControlProven by
Spoofing identitySomeone acts as an officer or commanderSanctum sessions, server side session verification, CSRF protection, role-scoped loginAuth feature tests + session verify flow
TamperingEvidence photo or record altered after captureSHA-256 of real bytes + append only chain of custody + verify actionEvidence integrity tests
RepudiationOfficer denies approving / editing a caseAudit log + status history written atomically with each changeAudit & workflow tests
Information disclosureOfficer reads another station's casesBackend data-scope enforcement (self/station/county/region/national) on every queryRBAC boundary tests (HTTP-level)
Denial of serviceAbuse of public endpoints / tilesRate limits, request timeouts, bounded per_page, caching, healthchecksLoad & validation tests
Elevation of privilegeOfficer approves their own case; sub-entity edits cross scopeSegregation of duties on approval; scope checks on all sub-entity routes (fixed + regression tested)RBAC conformance suite
Web app abuseXSS, CSRF, SQL injection, mass assignmentFramework escaping + CSP posture, CSRF tokens, prepared statements/ORM, explicit fillable listsFramework + OWASP-oriented review
Secret leakageKeys shipped in repo or client bundleNo secrets in code; tile/API keys server side only; CI secret scanning postureRepo hygiene + env model
Data lossServer dies / disk fails / human errorPostgres dumps + volume snapshots, object replication to cold tier, tested restore drillsDR 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.
◆Design posture: assume an attacker may hold a valid officer account, a lost device may reconnect late, and any provider may be down. Every control above is enforced in the backend; the UI only mirrors it for usability.
Reference Manual · Deep Dive §16

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.

16

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_id used 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

SignalWhat it tells usExample rule
HTTP error rateBroken requests on the APIAlert if more than 1% of requests fail for 5 minutes
LatencySlow responses for officersAlert if the 95th percentile is above 1 second for 10 minutes
Outbox ageSync or side effects are stuckAlert if the oldest outbox event is older than 5 minutes
Sync successDevices failing to reconcileAlert if successful syncs drop below 99% over an hour
Queue depthWorkers falling behindAlert if queue length keeps growing beyond a set depth
Database and diskCapacity and bloatAlert on disk above 80%, long running queries, connection saturation
Backup freshnessRecovery is not actually possibleAlert 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.
◆Where this fits: monitoring containers join the compose stack in the operations diagram, the tooling decision is defended in § 12, alerts feed the threat model detection controls, and releases are verified against these metrics in § 14.
Reference Manual · Deep Dive §17

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.

17

Backend layout principles

  • Shared application code lives in app; every feature lives in its own folder under Modules.
  • 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 under components/ui; product UI under components/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 forWhere to look
An API route and its controllerModules/<module>/Http/Routes and Http/Controllers
Business rules for an operationModules/<module>/Domain/Actions
Validation for one endpointModules/<module>/Http/Requests
Schema for a tableModules/<module>/Database/Migrations
Models and relationsModules/<module>/Models
A screen, its form and its testssrc/features/<feature>
The role and permission mapsrc/lib/taeds/permissions.ts on the frontend, guarded Actions in the backend
Offline storage and syncsrc/lib/taeds/db.ts (Dexie database) and src/lib/taeds/sync.ts (outbox sync) on the frontend
Map rendering and basemap stylesrc/lib/taeds/map-style.ts (MapLibre GL + OpenFreeMap URL) and map components under src/components/taeds
The API client codesrc/shared/api/generated (regenerated, never edited by hand)
How one request flows through filesroute file → controller → request → Domain/Actions action → model → response resource
◆Why it is arranged this way. A developer can open one module or one feature folder and understand the whole change. Boundaries are enforced by tests, generated code can never be drifted by hand, and the same folder rules apply from the first pilot to the national rollout, so the codebase stays easy to reason about as it grows.
Appendix · Reference Material

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.

A

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 · /escalate and /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 chapterWhere 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
◆Non-negotiable rules (from the engineering constitution): security before convenience · PostgreSQL is the only truth · backend auth is the real boundary · offline first in every feature · sync is durable and idempotent · evidence is evidential · OpenAPI is the contract · TypeScript strict · modules keep boundaries · no secrets in the repo.