Production-grade multi-tenant backend for real-time field workforce tracking — attendance, GPS, expense management, and admin analytics.
FieldTrack 2.0 is a production-ready REST API for managing field workforce operations. It provides secure, multi-tenant APIs for tracking employee attendance, real-time GPS location, expense workflows, and aggregate analytics.
Boundaries: This repository is the API only. Infrastructure (nginx, monitoring stack, VPS provisioning) lives in the infra repository.
- Multi-tenant isolation — every query is scoped to the authenticated organization; cross-tenant access is architecturally impossible
- Attendance sessions — check-in / check-out lifecycle with state machine enforcement
- Real-time GPS ingestion — single and batch endpoints (up to 100 points), idempotent upsert, per-user rate limiting
- Async distance calculation — BullMQ background worker computes Haversine distance after check-out; never blocks the HTTP response
- Expense workflow — PENDING → APPROVED / REJECTED lifecycle, with re-review guard
- Admin analytics — org-wide summaries, per-user breakdowns, configurable leaderboard
- Redis-backed rate limiting — per-JWT-sub limits survive corporate NAT and horizontal scaling
- Security — Helmet, CORS, Redis rate limiter, brute-force detection
- Distributed tracing — OpenTelemetry → OTLP; trace IDs injected into every Pino log line
- Blue-green zero-downtime deployments — nginx upstream swap, health-check gate, 5-SHA rollback history
- Full test suite — Vitest unit + integration coverage; CI blocks deploy on failure
| Layer | Technology |
|---|---|
| Runtime | Node.js 24 (Debian slim / distroless) |
| Language | TypeScript 5.9 (strict, ESM) |
| Framework | Fastify 5 |
| Database | PostgreSQL via Supabase |
| Auth | JWT (@fastify/jwt) — Supabase-issued tokens |
| Job Queue | BullMQ + Redis |
| Validation | Zod 4 |
| Tracing | OpenTelemetry (OTLP export) |
| Security | @fastify/helmet · @fastify/cors · @fastify/rate-limit · @fastify/compress |
| Testing | Vitest |
| CI/CD | GitHub Actions → GHCR → Blue-Green VPS Deploy |
Prerequisites: Node.js ≥ 24, npm, a running Redis instance, a Supabase project
# Install dependencies
npm install
# Configure environment
cp .env.example .env
# Edit .env — fill in SUPABASE_URL, keys, REDIS_URL, and CORS_ORIGIN
# Start in development mode (hot reload)
npm run devThe API will start on http://localhost:3000.
All variables are validated at startup by src/config/env.ts (Zod schema, fail-fast).
| Variable | Required | Purpose |
|---|---|---|
API_BASE_URL |
✅ | Canonical public URL of this API (https://…, no trailing slash) |
APP_BASE_URL |
✅ | Root URL of the application — used in email footers and redirects |
FRONTEND_BASE_URL |
✅ prod | URL of the web frontend — used to build email links |
| Variable | Required | Default | Purpose |
|---|---|---|---|
CONFIG_VERSION |
✅ | "1" |
Schema version guard — must be "1" |
APP_ENV |
✅ | development |
Application environment — drives all app-level logic |
PORT |
✅ | 3000 |
Container listen port |
| Variable | Required | Purpose |
|---|---|---|
SUPABASE_URL |
✅ | Supabase project URL |
SUPABASE_ANON_KEY |
✅ | Supabase public/anon key |
SUPABASE_SERVICE_ROLE_KEY |
✅ | Service role key — bypasses RLS, never expose to clients |
SUPABASE_JWT_SECRET |
✅ | JWT signing secret (≥ 32 chars, HS256) |
REDIS_URL |
✅ | Redis connection URL (redis:// or rediss://) |
| Variable | Required in Prod | Default | Purpose |
|---|---|---|---|
CORS_ORIGIN |
✅ | "" |
Comma-separated allowed CORS origins. Empty activates localhost fallback in dev |
METRICS_SCRAPE_TOKEN |
✅ | — | Token required to scrape /metrics. Unset = open in dev/test |
TEMPO_ENDPOINT |
— | http://tempo:4318 |
OTLP HTTP endpoint for trace export |
Observability variables (
METRICS_SCRAPE_TOKEN,TEMPO_ENDPOINT) are optional for standalone operation. The API starts and handles requests without them.METRICS_SCRAPE_TOKENgates the/metricsendpoint (unset = endpoint is open, safe in dev/test).TEMPO_ENDPOINTcontrols where traces are exported; if the Tempo collector is unreachable, traces are silently dropped with no impact to request handling. The monitoring stack that scrapes these endpoints is managed in the [infra repository].
| Command | Purpose |
|---|---|
npm run dev |
Start development server with hot reload |
npm run typecheck |
TypeScript type check (no emit) |
npm test |
Run full test suite (Vitest) |
npm run build |
Compile TypeScript to dist/ |
npm start |
Start compiled production server |
./scripts/deploy.sh <sha> |
Blue-green deploy a specific image SHA |
./scripts/deploy.sh --rollback |
Interactive rollback to previous SHA |
./scripts/deploy.sh --rollback --auto |
Non-interactive rollback (CI) |
| Endpoint | Purpose | Deploy Gate |
|---|---|---|
GET /health |
Liveness check — returns {"status":"ok"} once the server bootstraps |
YES — used by deploy.sh and CI |
GET /ready |
Dependency check — verifies Redis and Supabase connectivity | NO — informational only, not a deploy gate |
/health returns 200 after server bootstrap regardless of dependency status. /ready failing does not block a deployment; a degraded-but-running API is preferred over a stuck deploy.
First-deployment requirement: The API container joins
api_network. On a fresh VPS, nginx (reverse-proxy) and Redis must already be running and attached to that network via the infra repository before the firstdeploy.shrun. Subsequent deploys are fully self-contained.
This API requires an external infra repository.
Expected on server (all under INFRA_ROOT=/opt/infra):
$INFRA_ROOT/docker-compose.nginx.yml— operator runs nginx from here$INFRA_ROOT/docker-compose.redis.yml— operator runs Redis from here$INFRA_ROOT/nginx/live,nginx/backup,nginx/api.conf— layout enforced bydeploy.shand readiness check- nginx container on
api_network; Redis atredis:6379onapi_network
Deployments run automatically via GitHub Actions on every push to master (after CodeQL scan passes).
CodeQL deep scan (master)
→ validate (typecheck + audit) ──┐
→ test-api ─────────────────────┼──► build-scan-push ──► vps-readiness-check ──► deploy
┘ │
api-health-gate ◄────────────────┘
│
health-and-smoke ──► rollback (on failure)
Blue-green strategy: The VPS always runs two containers (api-blue, api-green). On each deploy, the inactive slot is updated and nginx is reloaded to point at it. The previous slot is stopped only after the health gate passes.
nginx is managed by the infra repository. The API container joins api_network; nginx is expected to already be running and configured.
Manual deploy:
./scripts/deploy.sh <sha>Rollback:
./scripts/deploy.sh --rollbackSee docs/DEPLOYMENT.md for full deployment details.
api/
├── src/ # Application source
│ ├── modules/ # Domain modules (attendance · locations · expenses · analytics)
│ ├── plugins/ # Fastify plugins (JWT · metrics · security)
│ ├── workers/ # BullMQ distance calculation worker
│ ├── middleware/ # Auth + role guard
│ └── utils/ # Shared utilities (errors · response · tenant)
├── tests/ # Vitest unit and integration tests
├── scripts/ # Deploy, rollback, and utility scripts
├── docs/ # Project documentation
└── .github/workflows/ # GitHub Actions CI/CD
The web frontend is in a separate repository: fieldtrack-tech/web
Infrastructure (nginx, monitoring, VPS setup) is in a separate infra repository.
| Document | Description |
|---|---|
| Architecture | System design, component diagrams, data flows |
| API Reference | All endpoints, auth requirements, request/response schemas, error codes |
| Deployment Guide | VPS provisioning, CI/CD setup, blue-green deploy, troubleshooting |
| Rollback System | Rollback architecture, deployment history, safety features |
| Rollback Quick Reference | Fast operator reference card |
| Environment Contract | All environment variables, naming rules |
| Infra Contract | External infra responsibilities and path contract (INFRA_ROOT) |
| Changelog | Full history of every phase |
| Contributing | Contribution workflow, branching, code conventions |
| Security Policy | How to report vulnerabilities |
See CONTRIBUTING.md for setup instructions, branch naming conventions, and commit format.
Branch naming:
feature/<description> # new functionality
fix/<description> # bug fixes
docs/<description> # documentation
test/<description> # test additions
chore/<description> # maintenance / deps
Commit format:
type(scope): short imperative description
Allowed types: feat fix refactor ci docs test chore
All PRs require review from CODEOWNERS and must pass CI before merge.