Multi-tenant backend for a fleet communications and operations platform for ambulance services: companies, bases, ambulances, shifts, status, chat and per-tenant module billing.
Status: prototype / work in progress, not presented as production. The REST API is implemented; the realtime (Ably token) and radio (Agora token) endpoints that the clients call are not in this repository (see "Known gaps").
- Tenant hierarchy: company group > company > unit > base > ambulance (Prisma models in
prisma/schema.prisma). - Ambulance shifts, status codes, documents and destination commands.
- Chat and messages with read receipts (REST side).
- Module catalogue with billing types (fixed, per ambulance, per base, per driver-ambulance) enabled per company.
- External integrations record per company (a partner status feed and a drivers integration).
- Auth: sign-in, sign-out and auth-status routes with RS256 JWT in cookies.
- S3-compatible (Cloudflare R2) uploads through presigned URLs.
- OpenAPI document (
swagger.json) and a Scalar API reference at/docsin development.
flowchart LR
Dash[ambulance-fleet-dashboard<br/>web operator UI]
App[ambulance-fleet-app<br/>Expo mobile app]
Comms[ambulance-fleet-comms<br/>comms-only mobile app]
Driver[ambulance-fleet-driver<br/>driver mobile app]
Server[ambulance-fleet-api<br/>Fastify + Prisma]
PG[(PostgreSQL)]
R2[(R2 / S3 storage)]
Ably{{Ably realtime}}
Agora{{Agora RTC radio}}
Push{{Expo push}}
Dash -->|REST, Orval client| Server
App -->|REST, Orval client| Server
Comms -->|REST| Server
Driver -->|REST| Server
Server --> PG
Server --> R2
Dash <-->|chat, status events| Ably
App <-->|chat, status events| Ably
Comms <-->|chat| Ably
Driver <-->|events| Ably
Dash <-->|voice| Agora
App <-->|radio, PA| Agora
Comms <-->|radio| Agora
App --- Push
Comms --- Push
- The OpenAPI file is the contract: Orval generates typed React Query clients in the dashboard and the app from
swagger.json. - Clients obtain a short-lived Ably token request scoped to a tenant channel and an Agora token from the backend; the browser and phones then talk to Ably and Agora directly.
- Routes only validate and orchestrate; permissions live in
src/auth.
Layers inside this repo (src/app.ts registers hooks and src/http/routing/index.ts registers the route groups):
flowchart TD
Routing["Route groups<br/>src/http/routing/*"] --> Routes["One file per route, Zod schema<br/>src/http/routes/*"]
Routes --> AuthHook["JWT cookie/bearer hook<br/>src/http/hooks/auth.ts"]
Routes --> Casl["CASL abilities (company group, company)<br/>src/auth, src/http/helpers/casl.ts"]
Routes --> Prisma["Prisma client<br/>src/database/prisma.ts"]
Routes --> R2["R2 presigned URLs<br/>src/lib/cloudfare.ts"]
Prisma --> PG[(PostgreSQL)]
Env["Zod-validated env<br/>src/env"] -.-> Routes
Err["Error handler + exceptions<br/>src/error-handler.ts"] -.-> Routes
Sign-in and tenant scoping (src/http/routes/auth/sign-in.ts, src/http/hooks/auth.ts, src/http/routes/ambulances/get-ambulances.ts):
sequenceDiagram
participant C as Client
participant A as API
participant D as PostgreSQL
C->>A: POST /auth/sign-in (CPF, password)
A->>D: find user by document
A->>A: argon2 verify, sign RS256 JWT (30 min access, 7 day refresh)
A-->>C: tokens + accessToken cookie
C->>A: GET /ambulances (cookie or Bearer)
A->>A: auth hook verifies JWT
A->>D: query filtered by company group / company / unit / base from the token
A-->>C: tenant-scoped page
Data model summary (prisma/schema.prisma, 17 models; main entities only):
erDiagram
CompanyGroup ||--o{ Company : owns
Company ||--o{ Unit : has
Unit ||--o{ Base : has
Base ||--o{ Ambulance : hosts
Ambulance ||--o{ AmbulanceShift : runs
Ambulance ||--o{ AmbulanceStatus : reports
Ambulance ||--o{ AmbulanceDocuments : has
Ambulance ||--o{ Chat : "talks in"
Chat ||--o{ Messages : holds
Messages ||--o{ MessageReadReceipt : "read by"
Company ||--o{ CompanyModule : contracts
Module ||--o{ CompanyModule : "billed as"
CompanyGroup ||--o{ Integration : configures
User }o--o| Company : "belongs to"
- Multi-tenant RBAC with CASL: roles, subjects and tenant-scoped conditions in
src/auth/andsrc/http/helpers/casl.ts. - Zod type provider on every route, so validation, serialization and OpenAPI come from one schema (
src/http/routes/*). - Env validated at boot with zod (
src/env/index.ts). - Optional payload encryption hooks (
src/http/hooks/encrypt.ts,decrypt.ts), currently not registered insrc/app.ts. - One Prisma migration baseline for all tables (
prisma/migrations).
- No Ably-token, radio or WebSocket routes in this repo; the clients call them on a backend that is not part of this portfolio set, so the clients cannot be run end to end from these repositories alone.
- No automated tests and no CI.
- Both
pnpm-lock.yamlandyarn.lockare tracked; use pnpm.
Node, TypeScript, Fastify 5, zod 4, Prisma 6 (PostgreSQL), @fastify/jwt, CASL, argon2, AWS SDK v3 (R2), Biome.
Prerequisites: Node 22+, pnpm, Docker (for Postgres).
cp .env.example .env # fill placeholders
docker compose up -d # Postgres, needs POSTGRES_PASSWORD
pnpm install
pnpm prisma generate
pnpm prisma migrate dev
pnpm dev # http://localhost:3333, docs at /docs| Variable | Purpose |
|---|---|
PORT, ENV |
Listen port; development, staging or production |
DATABASE_URL |
PostgreSQL connection string |
CRYPTO_SECRET_KEY |
Key for the optional payload encryption |
JWT_PRIVATE_KEY, JWT_PUBLIC_KEY |
Base64 RS256 key pair |
AWS_* |
R2/S3 credentials, bucket, account, jurisdiction, public subdomain |
POSTGRES_* |
Used by docker-compose.yml only |
api.http holds sample requests (REST Client) with synthetic data.
None yet. Type-check with pnpm tsc --noEmit.
prisma/ schema and migrations
src/auth/ CASL abilities, roles, subjects
src/http/ routes, hooks, helpers, exceptions
src/env/ validated environment
src/lib/ storage client
swagger.json generated OpenAPI contract
- ambulance-fleet-app: mobile client
- ambulance-fleet-dashboard: operator dashboard
- ambulance-fleet-comms: communications app
- ambulance-fleet-driver: driver app
All rights reserved. Source available for portfolio review.