8fc88ffc14
Build & Push / Pipeline Tests (push) Failing after 59s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Failing after 51s
Test / API Unit Tests (push) Has been skipped
Test / Homepage Unit Tests (push) Has been skipped
Test / Carplace Unit Tests (push) Has been skipped
Test / Admin Unit Tests (push) Has been skipped
Test / Dashboard Unit Tests (push) Has been skipped
Test / API Integration Tests (push) Has been skipped
406 lines
16 KiB
Markdown
406 lines
16 KiB
Markdown
# API Architecture and Route Design
|
||
|
||
This document explains how the API is structured today, how requests move through the stack, and what each route group is responsible for.
|
||
|
||
Source of truth for the runtime wiring:
|
||
|
||
- `apps/api/src/app.ts`
|
||
- `apps/api/src/modules/*`
|
||
- `apps/api/src/middleware/*`
|
||
- `apps/api/src/swagger/openapi.ts`
|
||
|
||
## Runtime Overview
|
||
|
||
The API is an Express application mounted under `/api/v1` with a few non-versioned utility endpoints:
|
||
|
||
- `GET /health` returns process liveness.
|
||
- `GET /ready` returns readiness (PostgreSQL, Redis, storage). Returns `503` when a dependency fails.
|
||
- `GET /metrics` returns Prometheus-style ops metrics (request counts/latency, outbox gauges).
|
||
- `GET /docs` serves Swagger UI.
|
||
- `GET /api/v1/openapi.json` returns the generated OpenAPI document.
|
||
- `/storage/*` serves uploaded assets such as logos, hero images, vehicle photos, and reservation/customer documents.
|
||
|
||
A dedicated **`api-worker`** process runs notification outbox dispatch and scheduled jobs (Phase 1). The API process must not embed those jobs when multiple replicas are used (`ENABLE_EMBEDDED_JOBS` stays false). See `docs/design/RUNTIME_AND_OPS_SURFACE.md`.
|
||
|
||
The app boot sequence in `apps/api/src/app.ts` is intentionally ordered:
|
||
|
||
1. CORS is applied first.
|
||
2. storage guards are applied before static serving so private customer license files are never anonymously retrievable.
|
||
3. Swagger UI is mounted before Helmet so the UI assets are not blocked by CSP.
|
||
4. webhook routes that require raw payload handling are mounted before `express.json()`.
|
||
5. Helmet, request logging (structured JSON), metrics middleware, JSON parsing, and the module routers are mounted.
|
||
6. the centralized error middleware converts validation, Prisma, and application errors into consistent JSON responses.
|
||
7. Spoofable forwarded headers are scrubbed unless `TRUSTED_FORWARD_HEADERS=true` (see `docs/ops/proxy-trust.md`).
|
||
|
||
## Request Lifecycle
|
||
|
||
Most internal company routes follow the same pattern:
|
||
|
||
1. Express router receives the request.
|
||
2. Zod schemas validate `req.params`, `req.query`, and `req.body` through `parseParams`, `parseQuery`, and `parseBody`.
|
||
3. auth middleware resolves the caller identity.
|
||
4. tenant middleware resolves the company record.
|
||
5. subscription middleware blocks suspended or pending companies where required.
|
||
6. role middleware enforces employee or admin permissions.
|
||
7. the route handler calls a service function.
|
||
8. the service applies business rules and calls a repo or Prisma directly.
|
||
9. a presenter may normalize the payload for frontend consumption.
|
||
10. `ok()` or `created()` wraps successful responses in `{ "data": ... }`.
|
||
|
||
Common exceptions:
|
||
|
||
- `GET /health` and `GET /api/v1/docs` return raw JSON, not the `{ data }` envelope.
|
||
- webhook endpoints return minimal receipt payloads.
|
||
- some public endpoints return graceful fallback responses if the database is unavailable.
|
||
|
||
## Security and Access Model
|
||
|
||
There are four main access patterns.
|
||
|
||
### Employee JWT
|
||
|
||
`requireCompanyAuth` validates a Bearer token with `type === "employee"`, loads the employee, and attaches:
|
||
|
||
- `req.employee`
|
||
- `req.company`
|
||
- `req.companyId`
|
||
|
||
This is the default for dashboard/company management routes.
|
||
|
||
### Tenant Resolution
|
||
|
||
`requireTenant` reloads the company from `req.companyId` and guarantees the tenant context exists. Company-scoped modules then query by `companyId`.
|
||
|
||
### Subscription Guard
|
||
|
||
`requireSubscription` blocks company routes when the company status is `PENDING` or `SUSPENDED`. Public site and carplace routes are intentionally not behind this guard.
|
||
|
||
### Role-Based Access Control
|
||
|
||
Employee roles are hierarchical:
|
||
|
||
- `OWNER`
|
||
- `MANAGER`
|
||
- `AGENT`
|
||
|
||
Admin roles are hierarchical:
|
||
|
||
- `SUPER_ADMIN`
|
||
- `ADMIN`
|
||
- `SUPPORT`
|
||
- `FINANCE`
|
||
- `VIEWER`
|
||
|
||
### Renter JWT
|
||
|
||
`requireRenterAuth` validates a Bearer token with `type === "renter"` and attaches `req.renterId`.
|
||
|
||
`optionalRenterAuth` is used on carplace routes so public reads still work without a token.
|
||
|
||
### API Key
|
||
|
||
`requireApiKey` accepts a company public API key in `x-api-key`. This is the low-friction integration path for public company-site style calls.
|
||
|
||
## Implementation Pattern
|
||
|
||
The API codebase is organized per module. The most common pattern is:
|
||
|
||
- `*.routes.ts`: route definitions and middleware composition
|
||
- `*.schemas.ts`: Zod input validation
|
||
- `*.service.ts`: business rules and orchestration
|
||
- `*.repo.ts`: Prisma access helpers
|
||
- `*.presenter.ts`: response shaping
|
||
|
||
The `vehicles` module is representative:
|
||
|
||
- `vehicle.routes.ts` mounts auth, tenant, and subscription guards once for the router.
|
||
- route handlers validate input and call service functions.
|
||
- `vehicle.service.ts` contains business rules such as publish-state normalization and availability calculation.
|
||
- `vehicle.repo.ts` owns the Prisma queries and enforces tenant filters in reads.
|
||
- `vehicle.presenter.ts` shapes the response returned to clients.
|
||
|
||
Some modules split specialized workflows into additional services instead of one large file. The reservations module is the clearest example:
|
||
|
||
- `reservation.service.ts` handles CRUD and list behavior
|
||
- `reservation.lifecycle.service.ts` handles confirm/checkin/checkout/close/extend/cancel transitions
|
||
- `reservation.inspection.service.ts` handles pickup/dropoff inspections
|
||
- `reservation.additional-driver.service.ts` handles approval workflows
|
||
- `reservation.photo.service.ts` handles pickup/dropoff photo uploads
|
||
- `reservation.document.service.ts` generates contract and billing payloads
|
||
|
||
## Route Groups
|
||
|
||
The table below describes the current route groups mounted in `app.ts`.
|
||
|
||
| Base path | Access | Purpose | Notes |
|
||
| --- | --- | --- | --- |
|
||
| `/health` | Public | Process health | Non-versioned utility endpoint |
|
||
| `/docs` | Public | Swagger UI | Backed by `openapi.ts` |
|
||
| `/api/v1/openapi.json` | Public | OpenAPI JSON | Generated from Zod schemas and manual path entries |
|
||
| `/api/v1/auth/company` | Public | Company signup | `complete-signup` and `verify-email` now return disabled errors |
|
||
| `/api/v1/auth/employee` | Public plus employee JWT | Employee login, password reset, profile, language | Dashboard/staff authentication path |
|
||
| `/api/v1/auth/renter` | Renter JWT for profile routes | Renter profile and push token | `signup` and `login` are currently disabled in this codebase |
|
||
| `/api/v1/admin` | Admin JWT | Platform operations | Includes company admin, billing, subscriptions, pricing, audit, and admin-user management |
|
||
| `/api/v1/carplace` | Public, optional renter JWT | Carplace discovery and reservation intake | Designed for discovery and lead capture across companies |
|
||
| `/api/v1/site` | Public | White-label company site APIs | Drives each company-branded booking site |
|
||
| `/api/v1/subscriptions` | Mixed | Plan listing, webhooks, subscription lifecycle | Public, webhook, and authenticated sub-routers share the same prefix |
|
||
| `/api/v1/vehicles` | Employee JWT + tenant + subscription | Fleet management | Includes photos, status, calendar blocks, maintenance, and availability |
|
||
| `/api/v1/reservations` | Employee JWT + tenant + subscription | Reservation operations | Central booking lifecycle for internal staff |
|
||
| `/api/v1/team` | Employee JWT + tenant + subscription | Employee/team management | Owner-controlled invite, role, activation, removal |
|
||
| `/api/v1/customers` | Employee JWT + tenant + subscription | Company CRM and license handling | Includes protected license-image retrieval and approval flows |
|
||
| `/api/v1/offers` | Employee JWT + tenant + subscription | Promotional offers | Company marketing and promo codes |
|
||
| `/api/v1/analytics` | Employee JWT + tenant + subscription | Dashboard metrics and reports | Includes summary, dashboard, source, and report routes |
|
||
| `/api/v1/notifications` | Employee or renter JWT depending on route | Notification inbox and preferences | Separate company and renter surfaces |
|
||
| `/api/v1/companies` | Employee JWT + tenant + subscription | Company profile and settings | Brand, domains, contract settings, insurance policies, pricing rules, accounting, API key |
|
||
| `/api/v1/payments` | Mixed | Rental payment webhooks and company payment actions | Webhooks are public; operational routes are company-authenticated |
|
||
| `/api/v1/reviews` | Employee JWT + tenant + subscription | Review moderation | Company-side review visibility and replies |
|
||
| `/api/v1/complaints` | Employee JWT + tenant + subscription | Complaint handling | Internal complaint tracking |
|
||
| `/api/v1/webhooks` | Public | External webhook receivers | Currently includes `/clerk` placeholder |
|
||
|
||
## Functional Breakdown by Module
|
||
|
||
### Company and employee onboarding
|
||
|
||
- `POST /api/v1/auth/company/signup` creates the company tenant, owner employee, default subscription state, and related baseline records in one transaction.
|
||
- The signup flow generates a unique company slug, hashes the owner password, starts a 30-day trial window, and sends account-created notifications.
|
||
- Employee auth supports login, forgot-password, reset-password, profile read, and language updates.
|
||
|
||
### Company profile and business configuration
|
||
|
||
The `companies` module owns tenant-level settings:
|
||
|
||
- company identity
|
||
- brand settings and asset upload
|
||
- subdomain/custom-domain configuration
|
||
- contract settings used by rental documents
|
||
- insurance policies
|
||
- pricing rules
|
||
- accounting settings
|
||
- public API key generation/regeneration
|
||
|
||
This module is where most long-lived company configuration lives.
|
||
|
||
### Fleet management
|
||
|
||
The `vehicles` module handles:
|
||
|
||
- CRUD for vehicles
|
||
- publish/unpublish
|
||
- explicit status updates
|
||
- photo upload and deletion
|
||
- availability checks
|
||
- monthly vehicle calendar events
|
||
- manual calendar blocks
|
||
- maintenance logs
|
||
|
||
Uploads are persisted through the storage service, then surfaced under `/storage/*`.
|
||
|
||
### Reservations and rental workflow
|
||
|
||
The reservation aggregate is the center of the operational API.
|
||
|
||
Important transitions:
|
||
|
||
1. create draft reservation
|
||
2. confirm reservation after license checks
|
||
3. check in and move the reservation to `ACTIVE`
|
||
4. check out and mark the reservation `COMPLETED`
|
||
5. close the reservation operationally after post-rental tasks are done
|
||
|
||
The lifecycle service also handles:
|
||
|
||
- extensions with conflict checks
|
||
- cancellations
|
||
- vehicle status synchronization
|
||
- review token generation
|
||
- review request email dispatch on close
|
||
|
||
The reservations module also owns:
|
||
|
||
- contract and billing payload generation
|
||
- pickup/dropoff inspections
|
||
- additional-driver approval
|
||
- pickup/dropoff photo upload
|
||
|
||
### Customer CRM and license compliance
|
||
|
||
Customers are company-scoped CRM records, even when a renter identity also exists. The `customers` module supports:
|
||
|
||
- customer CRUD
|
||
- flag/unflag flows
|
||
- license image upload and protected retrieval
|
||
- license validation and manager approval
|
||
|
||
This is intentionally separate from renter identity because one renter may interact with multiple companies while each company still needs its own operational customer record.
|
||
|
||
### Carplace and public site
|
||
|
||
The public surface is split in two modules on purpose.
|
||
|
||
`carplace` is the cross-company discovery layer:
|
||
|
||
- featured/public offers
|
||
- carplace cities
|
||
- listed companies
|
||
- vehicle search
|
||
- carplace reservation intake
|
||
- review submission by token
|
||
- company public pages under `/:slug`
|
||
|
||
`site` is the white-label company booking API:
|
||
|
||
- company brand payload
|
||
- published vehicle catalog
|
||
- booking options
|
||
- date-range availability
|
||
- promo validation
|
||
- booking creation
|
||
- payment initialization
|
||
- booking lookup
|
||
- company contact form
|
||
|
||
Both modules deliberately include graceful degradation paths when the database is unavailable so public pages can fail softer than internal operations.
|
||
|
||
### Payments
|
||
|
||
Rental payments are company-side operational payments, distinct from SaaS subscription billing.
|
||
|
||
The `payments` module handles:
|
||
|
||
- AmanPay and PayPal rental webhooks
|
||
- listing payments by company or reservation
|
||
- initializing online charges
|
||
- capturing PayPal orders
|
||
- recording manual payments
|
||
- refunding successful online payments
|
||
|
||
Payment success updates both the payment record and the reservation paid amount.
|
||
|
||
### Subscriptions and SaaS billing
|
||
|
||
The `subscriptions` module owns the platform billing lifecycle:
|
||
|
||
- public plan/provider/feature listing
|
||
- company subscription read endpoints
|
||
- trial start
|
||
- checkout
|
||
- plan changes
|
||
- cancellation/resume/reactivation
|
||
- provider webhooks
|
||
- admin overrides
|
||
|
||
It also exposes background-job style functions for:
|
||
|
||
- trial expiration
|
||
- payment-pending timeout
|
||
- past-due timeout
|
||
- suspension timeout
|
||
- period-end cancellation
|
||
|
||
### Notifications
|
||
|
||
Notifications are multi-channel and multi-audience:
|
||
|
||
- company inbox
|
||
- renter inbox
|
||
- unread counts
|
||
- preferences per type and channel
|
||
- notification history
|
||
|
||
Templates and preference records live in the database; delivery orchestration lives in `services/notificationService.ts`.
|
||
|
||
### Admin surface
|
||
|
||
The admin router is broad because it covers platform operations across multiple domains:
|
||
|
||
- admin authentication and 2FA
|
||
- company listing, inspection, status changes, deletion, impersonation
|
||
- renter blocking/unblocking
|
||
- platform metrics
|
||
- notification inspection
|
||
- audit log access
|
||
- admin-user and permission management
|
||
- billing account and billing invoice operations
|
||
- pricing config, plan features, and promotions
|
||
- subscription overrides and extensions
|
||
- carplace homepage configuration
|
||
|
||
This is the only route group allowed to work across tenants.
|
||
|
||
## Important Workflow Examples
|
||
|
||
### 1. Company signup
|
||
|
||
`POST /api/v1/auth/company/signup`
|
||
|
||
- validates the payload with Zod
|
||
- checks for duplicate company and owner email addresses
|
||
- generates a unique slug
|
||
- hashes the owner password
|
||
- creates the tenant, owner employee, and initial subscription state in one transaction
|
||
- sends localized account-created notifications
|
||
|
||
### 2. Public booking from a company site
|
||
|
||
`POST /api/v1/site/:slug/book`
|
||
|
||
- resolves the company from the slug
|
||
- verifies vehicle availability for the requested date range
|
||
- upserts the company-scoped customer record
|
||
- computes total days and base rental price
|
||
- applies promo code discount if present
|
||
- applies pricing rules
|
||
- stores a draft reservation
|
||
- attaches reservation insurance snapshots and additional-driver snapshots
|
||
- triggers license validation flags
|
||
|
||
Payment is then started with `POST /api/v1/site/:slug/booking/:id/pay`.
|
||
|
||
### 3. Reservation lifecycle after booking
|
||
|
||
- reservation starts in `DRAFT`
|
||
- staff confirms it after license compliance checks
|
||
- vehicle status moves to `RESERVED`
|
||
- checkin moves reservation to `ACTIVE` and vehicle to `RENTED`
|
||
- checkout moves reservation to `COMPLETED`, records mileage, and generates a review token
|
||
- close marks the operational workflow complete and can send the review request email
|
||
|
||
## Error Handling and Response Shape
|
||
|
||
The API standardizes error handling in `http/errors/errorMiddleware.ts`.
|
||
|
||
Special handling exists for:
|
||
|
||
- Zod validation errors -> `400 validation_error`
|
||
- Prisma `P2025` -> `404 not_found`
|
||
- Prisma `P2002` -> `409 conflict`
|
||
- custom `AppError` subclasses -> explicit status and machine-readable code
|
||
|
||
Successful route handlers usually return:
|
||
|
||
```json
|
||
{
|
||
"data": {}
|
||
}
|
||
```
|
||
|
||
Common deviations:
|
||
|
||
- `/health`
|
||
- `/ready`
|
||
- `/metrics`
|
||
- `/api/v1/docs`
|
||
- webhook receipt payloads
|
||
- file downloads such as admin invoice PDFs
|
||
|
||
## Documentation Artifacts
|
||
|
||
There are two documentation layers in the codebase:
|
||
|
||
1. runtime API docs from Swagger/OpenAPI at `/docs` and `/api/v1/openapi.json`
|
||
2. this markdown document, which explains design decisions, route grouping, and request flow
|
||
|
||
Production-readiness design updates (Phases 0–4) are summarized under `docs/design/`.
|
||
|
||
The OpenAPI file is useful for request/response contracts. CI enforces a minimum coverage gate via `npm run openapi:coverage`. This document should be updated together with `app.ts`, route files, and the OpenAPI generator whenever route groups change.
|