All significant changes to FieldTrack 2.0 are documented here by development phase.
- Created
src/plugins/zod.plugin.ts— single exportedregisterZod(app)helper that callssetValidatorCompiler+setSerializerCompiler; this is now the only place these are set, eliminating the previous drift between production and test environments - Updated
src/app.tsto callregisterZod(app)at the root level before any plugins or routes are registered - Removed the duplicate
setValidatorCompiler/setSerializerCompilercalls that were previously insideopenapi.plugin.ts - Updated
tests/setup/test-server.tsto callregisterZod(app)from the shared plugin instead of inline copies
- Moved
[authenticate, requireRole(...)]frompreHandlertopreValidationon all routes that carry a ZodbodyorquerystringschemaPOST /expenses,PATCH /admin/expenses/:id,GET /expenses/my,GET /admin/expensesGET /attendance/my-sessions,GET /attendance/org-sessionsPOST /locations,POST /locations/batch,GET /locations/my-route- All three analytics admin endpoints
- This ensures
401 Unauthorized/403 Forbiddenalways fires first, before Zod ever runs, matching the expected HTTP semantics
- Updated
setErrorHandlerin bothsrc/app.tsandtests/setup/test-server.tsto pass through Fastify's built-in4xxerrors (validation errors, rate-limit 429, etc.) instead of collapsing them to 500
- Added missing
body: createLocationSchematoPOST /locations - Added missing
body: createLocationBatchSchematoPOST /locations/batch - Added missing
querystring: sessionQuerySchematoGET /locations/my-route - These schemas were previously only applied inside the controller; moving them to the route definition makes them visible to OpenAPI/Swagger generation
- Added
removeOnComplete: { count: 1000 }andremoveOnFail: { count: 5000 }to the distance worker constructor - Prevents unbounded Redis memory growth from accumulating stale job records
- Bumped
Dockerfilefromnode:20-alpine→node:24-alpine(both builder and production stages) - Added
"engines": { "node": ">=24.0.0" }topackage.json
- Upgraded
@opentelemetry/auto-instrumentations-node:^0.55.0→^0.71.0 - Upgraded
@opentelemetry/exporter-trace-otlp-http:^0.57.0→^0.213.0 - Upgraded
@opentelemetry/sdk-node:^0.57.0→^0.213.0 - Added
@opentelemetry/resources@^2.0.0and@opentelemetry/sdk-trace-base@^2.0.0as explicit dependencies (previously transitive) - Updated
src/tracing.ts: replacednew Resource({ ... })(class, removed in v2) withresourceFromAttributes({ ... })factory; replaced deprecatedSEMRESATTRS_*constants (removed insemantic-conventions@1.40) with stable string literals ("service.name","service.version","deployment.environment")
- Test count increased from 124 → 125 passing tests
- All existing integration and unit tests continue to pass after the lifecycle and compiler changes
- Split GitHub Actions
deploy.ymlinto two jobs:test(runs on all events) andbuild-and-deploy(push to master only,needs: test) - Replaced
npm installwithnpm cifor deterministic dependency installs - Added
npx tsc --noEmittype-check step before tests in CI - Added
actions/setup-nodecache keyed onpackage-lock.jsonto avoid redundant installs - Upgraded Docker build to
docker/build-push-actionwith GitHub Actions layer cache (type=gha) - PRs to
masternow run thetestjob — failing tests block merge - Every image is tagged with both
latestand a 7-character SHA
- Added rollback mode to
scripts/deploy.sh— reads.deploy_history, validates ≥ 2 deployments, displays history table with current/target markers, prompts for confirmation, and redeploys the previous image - Updated
scripts/deploy.shto prepend the deployed SHA to.deploy_history(rolling window of 5) after every successful deploy - Added
.gitignoreentry for.deploy_history - Added
docs/ROLLBACK_SYSTEM.mdanddocs/ROLLBACK_QUICKREF.md
- Added Vitest and
@vitest/coverage-v8to devDependencies - Added
vitest.config.ts(globals, node env,setupFiles) - Added
tsconfig.test.jsonextending base config withvitest/globalstypes - Added
tests/setup/env-setup.ts— sets all required env vars from test env - Added
tests/setup/test-server.ts— minimalbuildTestApp()factory with JWT plugin + routes (no Redis, no Prometheus, no BullMQ) - Added
tests/helpers/uuid.ts—TEST_UUID(),TEST_UUIDS(n),FIXED_TEST_UUIDhelpers (needed for Zod 4 strict UUID validation)
tests/unit/utils/pagination.test.ts—applyPagination()clamping, offsets, coerciontests/unit/utils/response.test.ts—ok(),fail(),handleError()dispatchtests/unit/utils/errors.test.ts— all custom error classes and inheritancetests/unit/services/attendance.service.test.ts—checkIn/checkOutbusiness rulestests/unit/services/expenses.service.test.ts—createExpense/updateExpenseStatusrole enforcementtests/integration/attendance/attendance.test.ts— check-in, check-out, my-sessions, org-sessions (401/403/400/201/200)tests/integration/expenses/expenses.test.ts— full CRUD flow including re-review guardtests/integration/locations/locations.test.ts— single insert, batch insert, my-route
src/utils/tenant.ts— addedTenantContexttype;enforceTenant()now accepts bothFastifyRequestand a plainTenantContextobject (enables worker-path usage without a fake request)src/middleware/role-guard.ts—requireRole()now throwsForbiddenErrorinstead of manually callingreply.status(403).send(), routing all auth failures through the centralizedhandleErrorpipelinesrc/routes/debug.ts— early return inproductionenvironment, preventing infrastructure disclosuresrc/config/env.ts— addedWORKER_CONCURRENCYenv var (default1)src/modules/locations/locations.schema.ts— documentedsequence_numbernullable design decision with planned migration SQL
- Added
applyPagination()utility (src/utils/pagination.ts) — centralised page/limit clamping with safe defaults; used by all list endpoints - Added
ok()/fail()/handleError()response helpers (src/utils/response.ts) — standardised JSON response shape across all controllers - Migrated all controllers to use
handleErrorpipeline eliminating scatteredtry/catchblocks - All list endpoints now return consistent
{ success: true, data: [...] }shape - Introduced
AppErrorhierarchy insrc/utils/errors.ts—UnauthorizedError,ForbiddenError,NotFoundError,BadRequestErrorand domain errors (EmployeeAlreadyCheckedIn,SessionAlreadyClosed,ExpenseAlreadyReviewed)
- Added
backend/migrations/phase16_schema.sqland Supabase migration20260309000000_phase16_schema.sql - Added PostgreSQL enum types for
attendance_statusandexpense_status - Added TypeScript DB type definitions (
src/types/db.ts) generated from the Supabase schema snapshot - Updated all repositories to use typed Supabase query responses
- Locked repository return types throughout — no more
anycasts on DB results sequence_numbercolumn added togps_locations(nullable, pending mobile stabilization)
- Extracted security concerns into dedicated plugins under
src/plugins/security/helmet.plugin.ts—@fastify/helmetwith CSP deferred pending frontend enumerationcors.plugin.ts—@fastify/corswithALLOWED_ORIGINSenv andcredentials: trueratelimit.plugin.ts— Redis-backed global 100 req/min,127.0.0.1/::1allowlistedabuse-logging.plugin.ts— structured 429 logging; brute-force detection on auth routes
- Added two new Prometheus counters:
security_rate_limit_hits_total{route},security_auth_bruteforce_total{ip} - Rate-limit Redis connection is separate from the BullMQ Redis connection
- Added
src/tracing.ts— OpenTelemetry Node.js SDK with OTLP HTTP exporter to Tempo;fsinstrumentation disabled to reduce noise; must be the first import inserver.ts - Added
otelMixininsrc/config/logger.ts— injectstrace_id,span_id,trace_flagsinto every Pino log line - Added OTel span enrichment in
app.tsonRequesthook — setshttp.route,http.client_ip,request.id,enduser.idon every request - Upgraded Prometheus histogram to
observeWithExemplar()withtraceIdon every observation - Updated standalone infra repository monitoring config — Tempo ports 4317/4318; Prometheus
--enable-feature=exemplar-storage - Updated standalone infra repository Prometheus config — OpenMetrics scrape format for exemplar ingestion
- Added VPS setup and infra assets for production infrastructure (later extracted into standalone infra repository)
- Added
src/plugins/prometheus.ts—prom-clientregistry,http_request_duration_secondshistogram,http_requests_totalcounter,bullmq_queue_depthgauge - Added
GET /metricsendpoint (OpenMetrics text format) - Iteratively fixed route labeling: stable pattern-based labels (
/users/:id) instead of raw URLs - Moved timing hook from
onSendtoonResponsefor accurate end-to-end latency measurement - Wrapped plugin with
fastify-pluginto escape encapsulation
- Added initial GitHub Actions workflow for automated deployment
- Added blue-green zero-downtime deployment script (later unified into
scripts/deploy.sh) - Health-check validation before traffic switch
- Old container removed only after successful switchover
- Added
ALLOWED_ORIGINSenv var and CORS configuration - Added
bodyLimit: 1_000_000(1 MB),connectionTimeout: 5_000,keepAliveTimeout: 72_000to Fastify - Added
requestIdHeader: "x-request-id"+genReqId: () => randomUUID()for request correlation - Added
x-request-idheader to every response viaonSendhook - Moved from in-process rate limiting to
@fastify/rate-limitplugin (in-process; later upgraded to Redis-backed in Phase 15) - Added Redis URL validation — throws on missing scheme instead of silently falling back
- Added
MAX_QUEUE_DEPTH,MAX_POINTS_PER_SESSION,MAX_SESSION_DURATION_HOURSsafety limits
- Added
src/modules/analytics/module (controller, service, repository, schema, routes) - Endpoints:
GET /admin/org-summary,GET /admin/user-summary,GET /admin/top-performers - Date range filtering with
from/toISO-8601 params - All endpoints ADMIN-only; no analytics data exposed to EMPLOYEE role
top-performerssupportsmetric=distance|duration|sessions,limit1–50
- Added
src/modules/expenses/module (controller, service, repository, schema, routes) - Expense lifecycle:
PENDING → APPROVED | REJECTED; only PENDING expenses can be transitioned - Added
ExpenseAlreadyRevieweddomain error - Protected
/internal/metricswithauthenticate + requireRole("ADMIN")— previously unauthenticated - Added
GET /internal/metricsendpoint returning queue depth, recalculation count, and latency averages
- Added
src/utils/metrics.ts— in-process counters fortotalRecalculations,totalLocationsInserted,avgRecalculationMs(rolling average of last 100 jobs) - Worker now recovers stale jobs on restart — jobs that were
activewhen the process crashed are retried automatically - Added concurrency and backoff configuration to the distance worker
- Added job deduplication in the queue — prevents duplicate recalculation jobs for the same session
- Added
src/workers/distance.queue.ts— BullMQ queue (distance-calculation) with Redis backend - Added
src/workers/distance.worker.ts— processes jobs: fetches GPS points → Haversine calculation → upsertssession_summaries POST /attendance/check-outnow enqueues a BullMQ job instead of calculating synchronously (eliminates HTTP timeout risk on long sessions)- Added
src/modules/session_summary/module — service and repository for reading/writingsession_summariestable - Added
POST /attendance/:sessionId/recalculateroute for manual re-triggering
- Added
src/utils/distance.ts— Haversine formula implementation (calculateDistance,haversine) - Added
session_summariestable to schema (storestotal_distance_km,total_duration_secondsper session) - Distance calculated synchronously on check-out (later moved to async queue in Phase 7)
- Added
session_summaryservice and repository
- Added JWT-sub-based
keyGeneratortoPOST /locationsandPOST /locations/batch— rate limits are per identity, not per IP - Added
performance.now()latency tracking in the locations service; logged aslatencyMson every insert - Added
duplicatesSuppressedmetric in batch insert — logs the difference between submitted and actually-inserted points
- Added
POST /locations/batchendpoint accepting up to 100 GPS points per request - Added
createLocationBatchSchema— shared point schema reused from single-insert,session_idhoisted to root - Batch insert uses Supabase
upsertwithignoreDuplicates: trueto handle mobile client retries safely
- Added
src/modules/locations/module (controller, service, repository, schema, routes) POST /locations— single GPS point ingestion with Zod validation (coordinate bounds, accuracy ≥ 0,recorded_atnot more than 2 min in future)GET /locations/my-route— returns all GPS points for a session, ordered byrecorded_atASCgps_locationstable uses composite upsert key(session_id, recorded_at)for idempotency
- Added
src/modules/attendance/module (controller, service, repository, schema, routes) POST /attendance/check-in— createsACTIVEsession; throwsEmployeeAlreadyCheckedInif one existsPOST /attendance/check-out— closesACTIVEsession; throwsSessionAlreadyClosedif none existsGET /attendance/my-sessions— paginated list of own sessionsGET /attendance/org-sessions— ADMIN-only paginated list of all org sessions- Tenant isolation enforced in every repository query via
organization_id
- Added
src/middleware/auth.ts— JWT verification via@fastify/jwt+ Zod payload validation; attachesrequest.organizationId - Added
src/middleware/role-guard.ts—requireRole(role)preHandler factory; throwsForbiddenErrorfor mismatched roles - Added
src/types/jwt.ts+jwtPayloadSchema— strict Zod schema for JWT claims (sub,organization_id,role) - Added
src/utils/tenant.ts—enforceTenant()for ensuring repository queries are always scoped to the authenticated organization - Added
src/plugins/jwt.ts— registers@fastify/jwtwithSUPABASE_JWT_SECRET
- Initialized Node.js + TypeScript 5.9 project (strict mode, ESM
"module": "NodeNext") - Added Fastify 5 with
fastify-plugin - Added Supabase client (
@supabase/supabase-js) configured insrc/config/supabase.ts - Added Pino structured logging with environment-aware config (
src/config/logger.ts) - Added
src/config/env.ts— centralized environment variable loading with fail-fast validation - Added
src/config/redis.ts— ioredis client for BullMQ - Added
backend/Dockerfile— multi-stage build (build → production Alpine image) - Added
.env.examplewith all required variable names