Skip to main content

Itinera — Roadmap

Last updated: February 27, 2026 — Phase 3 Complete: Schema foundations established. Route enrichment fields, vehicle profiles table, notification indexes, PostGIS extension, and user Samsara linking all deployed.

Cloudflare services in use:

  • R2 — free backup storage (Phase 0)
  • DNS — move from Hostinger to Cloudflare (Phase 0)
  • Proxy + CDN — hides droplet IP, free DDoS protection (Phase 0)
  • WAF — blocks common attack patterns at edge (Phase 0)
  • Email Routing — forward support@getitinera.com to personal inbox (Phase 0)
  • Tunnel — replaces Nginx as reverse proxy, closes all inbound ports (Phase 1)
  • Pages — hosts static getitinera.com homepage, auto-deploys from GitHub (Phase 1)

Rule: Finish each phase completely before starting the next. The temptation to run phases in parallel is how projects stall with half-finished migrations.


Phase Summary

PhaseNameStatusGate
0Fix Live Risks✅ CompleteDo today — no prerequisites
1Operational Stability✅ CompleteBefore onboarding any real carriers
2Dependency Upgrades + Testing✅ CompleteBefore HERE migration
3Schema Foundations✅ CompleteHERE migration ready
4HERE Migration⏳ Phase 4After Phases 2 & 3
5Fuel Card Integration + Real Analytics⏳ Phase 5After HERE
6Minio — File Storage⏳ Phase 6After Phase 3 (permits schema)
7n8n — Workflow Automation⏳ Phase 7After Phases 5 & 6 (real data)
8Multitenancy + Better Auth⏳ Phase 8Only when second carrier is ready
9Driver Portal + DRIVER Role⏳ Phase 9After multitenancy

Phase 0 — Fix Live Risks (Do Today)

No new features. No app code changes. Eliminate active security and operational risks.

Cloudflare setup (do this first — unlocks R2, proxy, WAF, email routing)

  • Create free Cloudflare account at cloudflare.com
  • Add getitinera.com to Cloudflare (add as site, choose free plan)
  • Copy the two Cloudflare nameservers shown
  • Replace Hostinger nameservers with Cloudflare nameservers (already set in Hostinger)
  • Wait for propagation — domain shows Active immediately
  • DNS records imported — A records for @, www, app → 45.55.214.61 all proxied
  • Proxy (orange cloud) enabled on getitinera.com, www.getitinera.com, app.getitinera.com
  • SSL/TLS mode set to Full (strict)
  • https://app.getitinera.com loads correctly

Cloudflare WAF (managed OWASP ruleset requires Pro — free tier gets custom rules only)

  • Create custom rule: Block all requests where URI path contains /api/cron/ → Block action deployed
  • (Pro only) Enable OWASP Core Ruleset for SQLi, XSS, path traversal protection at edge

Off-site backups (Cloudflare R2 — free)

  • In Cloudflare dashboard → R2 → create bucket itinera-backups (Standard, Eastern North America)
  • Create R2 Account API token (Object Read & Write, scoped to itinera-backups, IP locked to 45.55.214.61)
  • Install rclone on the droplet: curl https://rclone.org/install.sh | sudo bash
  • Configure rclone for R2: remote named r2, S3 provider Cloudflare, Access Key + Secret + endpoint
  • Update /var/backups/itinera/backup.sh to upload all .sql.gz files to R2 after each dump
  • Run backup script manually — file appears in R2 bucket confirmed
  • Test restore: gunzip -t on downloaded backup confirms file is valid

Google Maps API key hardening (temporary — key removed entirely in Phase 4 HERE migration)

  • Add HTTP referrer restrictions in Google Cloud Console: *.getitinera.com/* and app.getitinera.com/*
  • Verify map still loads after restrictions are applied
  • Confirm key is rejected from other origins

Security headers via Cloudflare Transform Rules (replaces Nginx headers — survives Phase 1 Coolify rebuild)

  • In Cloudflare dashboard → Rules → Transform Rules → Response Header Transform Rules
  • Deployed rule "Security headers" applying to all incoming requests with:
    • X-Frame-Options: DENY
    • X-Content-Type-Options: nosniff
    • Referrer-Policy: strict-origin-when-cross-origin
    • Permissions-Policy: geolocation=(), camera=(), microphone=()
    • Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval' *.googleapis.com *.gstatic.com
  • Verify headers with curl -I https://app.getitinera.com — all security headers confirmed present, served via Cloudflare

Cron endpoint protection (two layers)

  • Nginx layer: location /api/cron/ block → allow 127.0.0.1; deny all; added to /etc/nginx/sites-available/itinera
  • Cloudflare layer: WAF custom rule blocking URI path /api/cron/ deployed
  • Cron jobs updated to call http://localhost:3000 instead of https://app.getitinera.com
  • Verified: curl from external returns 403, localhost returns {"success":true}

Replace xlsx with exceljs

  • Uninstalled xlsx, installed exceljs
  • Rewrote fuel price upload parsing in /api/fuel-prices/route.ts using ExcelJS
  • Rewrote report export in report-utils.tstoXLSX and fileResponse now async
  • Updated all four /api/reports/* routes to await fileResponse(...)
  • Fixed buffer conversion: Buffer.from(new Uint8Array(arrayBuffer)) for ExcelJS compatibility
  • Build passes, deployed to production, fuel price upload tested and working

GitHub housekeeping

  • Added npm audit --audit-level=high --omit=dev step to GitHub Actions deploy workflow
  • Set audit to continue-on-error: true — Next.js DoS CVEs (GHSA-9g9p, GHSA-h25m) deferred to Phase 2 upgrade
  • Enable branch protection on master (skipped — not enforced on free private repos; revisit in Phase 8 when team grows)

Phase 1 — Operational Stability

Before onboarding any real carriers, establish infrastructure that makes the platform reliable and observable.

Cloudflare Pages (do before Coolify — move static site off the droplet first)

  • Created Pages project itinera-homepage connected to ioncernenchii/itinera, build output homepage, no build command
  • Set custom domains: getitinera.com and www.getitinera.com — both Active + SSL enabled
  • Verified getitinera.com loads correctly from Cloudflare Pages
  • Removed homepage sync step from GitHub Actions deploy workflow
  • Removed /etc/nginx/sites-available/getitinera and /etc/nginx/sites-enabled/getitinera
  • Deleted /var/www/getitinera.com/ from droplet
  • Nginx config tested and reloaded successfully

Coolify Migration

  • Dump PostgreSQL database and push to R2 (itinera_20260225_120836.sql.gz)
  • Upgrade DigitalOcean droplet to 4GB RAM / 2 vCPU (resized, not 8GB — sufficient for now)
  • Fresh Ubuntu 24.04 install on upgraded droplet
  • Install Coolify v4.0.0-beta.463 via official install script
  • Fixed Docker IPv6 bug (/etc/docker/daemon.json{"ipv6": false}) — PostgreSQL container was exiting
  • Add DNS A records in Cloudflare for coolify, app, secrets → 45.55.214.61
  • Configure Coolify domain (coolify.getitinera.com) with SSL
  • Deploy PostgreSQL 18 database (itinera-db) via Coolify
  • Connect GitHub repo via GitHub App (itinera-app) — private repo access
  • Deploy Itinera (Next.js) as Coolify service at app.getitinera.com using Nixpacks
  • Restore PostgreSQL database from R2 backup
  • Baseline Prisma migrations (3 migrations marked as applied)
  • Fixed env var runtime bug — updated Coolify DB directly (is_runtime = true for all vars)
  • App running and login confirmed working
  • Set up Infisical (self-hosted via Coolify at secrets.getitinera.com) — running healthy
  • Add all production secrets to Infisical production environment — source of truth
  • Create machine identity coolify with Universal Auth credentials
  • Connect Infisical to Coolify Deferred to Phase 8 — no native sync in Coolify beta.463; Infisical is reference/backup only for now; env vars managed directly in Coolify until multitenancy justifies full sync
  • Remove old GitHub Actions deploy workflow — replaced with ci.yml (lint + build check only; Coolify handles deploys)
  • Verify all subdomains and SSL certificates — all 4 subdomains returning 200/302 over HTTP/2
  • Verify cron jobs running; smoke test full platform
    • Host-level crontab chosen over Coolify Scheduled Tasks (container name changes on every redeploy; Coolify tasks have known reliability bugs)
    • Dynamic IP lookup: docker inspect with coolify.name label filter — survives redeploys
    • Both endpoints confirmed firing every 60s: 106 vehicles, 6 active routes, fuel + deviation alerts working
    • Logs: /var/log/cron-deviations.log and /var/log/cron-fuel.log

Cloudflare Tunnel

  • Install cloudflared on the droplet
  • Authenticate: cloudflared tunnel login
  • Create tunnel: cloudflared tunnel create itinera — ID: efa99bf0-1a82-43ed-8b32-06e454745179
  • Configure ingress: all 3 subdomains route through https://localhost:443 (Traefik) with noTLSVerify: true
  • Updated DNS CNAMEs in Cloudflare to point to efa99bf0-1a82-43ed-8b32-06e454745179.cfargotunnel.com
  • Run cloudflared as a system service: cloudflared service install — enabled + active
  • Created DigitalOcean firewall: inbound SSH only (port 22), all outbound allowed
  • Verified: direct IP 45.55.214.61 times out, all subdomains load normally via tunnel

Uptime Kuma

  • Deploy via Coolify at status.getitinera.com
    • Docker Compose service with louislam/uptime-kuma:1, persistent volume
    • Must be on coolify network (external) for Traefik to route traffic
    • Domain set to http://status.getitinera.com (not https) — Cloudflare handles TLS at edge; Traefik must not attempt Let's Encrypt since port 80/443 inbound is blocked
    • Tunnel config: status.getitinera.comhttp://localhost:80 (not 443) — Uptime Kuma only has HTTP route
    • Key lesson: /etc/cloudflared/config.yml is what the systemd service reads — NOT /root/.cloudflared/config.yml; deleted the root copy to avoid confusion
  • Monitor https://app.getitinera.com — Itinera App (HTTP, 60s, 2 retries)
  • Monitor https://app.getitinera.com/api/health/db — PostgreSQL health
  • Monitor https://app.getitinera.com/api/health/samsara — Samsara API health
    • Created /api/health, /api/health/db, /api/health/samsara endpoints
    • TCP port monitor not viable — DB container on isolated network, not reachable from Uptime Kuma
  • Configure Telegram notification channel — Itinera Infrastructure group (separate from fleet alerts)
  • All monitors wired to Telegram notification
  • Public status page at status.getitinera.com — all systems operational

Bugsink (Error Tracking)

  • Deploy via Coolify at errors.getitinera.com
  • Create project, copy DSN, add to Coolify env vars
  • Install @sentry/nextjs in app pointed at Bugsink DSN
  • Verify errors appear in dashboard
  • Migrate Sentry config to instrumentation.ts pattern
  • Configure Telegram webhook for critical errors
  • Fix Bugsink BASE_URL env var to https://errors.getitinera.com
  • Delete src/app/api/sentry-test/route.ts test endpoint

Persistent Rate Limiting

  • Install rate-limiter-flexible — used raw Prisma instead (no new deps, consistent with codebase)
  • Replace src/lib/rate-limit.ts in-memory store with PostgreSQL backing (rate_limits table)
  • Run npx prisma migrate deploy on server + verify rate limits survive a Coolify restart
  • Verify login lockout still works after deploy (5 failed attempts → blocked → restart → still blocked)

SUPERADMIN and READONLY Roles

  • Add SUPERADMIN and READONLY to UserRole enum in schema.prisma
  • Run Prisma migration (20260226000001_add_superadmin_readonly_roles)
  • Update requireAuth() and all API route role checks (new requireSuperAdmin(), isReadOnly() helpers)
  • Update Settings UI role badges and dropdowns (purple=SUPERADMIN, blue=ADMIN, grey=READONLY)
  • API enforcement: READONLY blocked from PATCH routes, DELETE requires requireSuperAdmin(), privilege escalation prevented

Phase 2 — Dependency Upgrades + Testing Foundation

Modernize the stack before HERE migration. Do not start Phase 4 until this is complete.

Testing Infrastructure (do first)

  • Install vitest and @vitest/coverage-v8
  • Install Husky, lint-staged, commitlint
  • Configure Husky pre-commit: run lint-staged (ESLint on changed files only)
  • Configure commitlint: enforce feat:, fix:, chore: convention
  • Write smoke tests: login flow, route CRUD, cron endpoint, fuel price upload
  • All tests passing before starting upgrades

Semantic Release (do alongside commitlint)

  • Install semantic-release and plugins: @semantic-release/commit-analyzer, @semantic-release/release-notes-generator, @semantic-release/changelog, @semantic-release/git, @semantic-release/github
  • Add .releaserc.json config — bump patch on fix:, minor on feat:, major on breaking changes; skip chore:, docs:, refactor:
  • Add GitHub Actions release.yml workflow: runs semantic-release on push to master
  • Verify: merging a feat: commit auto-bumps package.json, generates CHANGELOG.md, creates GitHub release
  • Outcome: version numbers and changelog are fully automated from commit messages — no manual bumping

Dependency Upgrades — follow UPGRADE_PLAN.md exactly:

  • Prisma 5 → 7 (with driver adapter pattern, DATABASE_URL moved to prisma.config.ts, @prisma/client imports updated)
  • TypeScript 5.6 → 5.9.3
  • Tailwind CSS 3 → 4 (CSS-first config, deleted tailwind.config.ts)
  • React 18 → 19 + Next.js 14.2.35 → 16.1.6 (with async params/cookies/headers codemod, serverExternalPackages renamed, ESLint 8 → 9 flat config)
  • NextAuth 4 → 5 / Auth.js v5 (new NextAuth() format, handlers/auth/signIn/signOut exports, CredentialsSignin error classes, getServerSession → auth())
  • Cleanup: lucide-react 0.460→0.575, zod removed (unused), @radix-ui/* all updated to latest, @auth/prisma-adapter removed (unused), uuid removed (unused)
  • Run full smoke test suite after each phase
  • Remove @auth/prisma-adapter if confirmed unused
  • Remove uuid package if confirmed unused
  • Consolidate validation: Zod everywhere OR keep lib/validation.ts — not both ✅ kept lib/validation.ts
  • Add GitHub Actions caching for node_modules and .next build cache
    • Remove continue-on-error from npm audit step in deploy.yml (was set as workaround for Next.js 14 CVEs — now fixed with Next.js 16 upgrade)

Build Optimization (GitHub Actions → Docker → Coolify)

  • Write a Dockerfile for the Itinera app (multi-stage: deps/builder/runner, Node 22 Alpine base, explicit npx prisma generate)
  • Add GitHub Actions workflow: build Docker image → push to ghcr.io with :latest, :sha-<short>, :<full-sha> tags
  • Switched Coolify from Nixpacks to Dockerfile build pack — builds from multi-stage Dockerfile locally on droplet
  • Set up ghcr.io auth on droplet: classic PAT with read:packages scope in /root/.docker/config.json
  • Created docs/GHCR_COOLIFY_DEPLOY.md with full setup + rollback instructions
  • Disk cleanup: freed 22GB Docker build cache + 19GB stale images → disk 99% → 30% (14GB used, 34GB free)
  • Added weekly cron: 0 3 * * 0 docker builder prune -af to prevent recurrence
  • Configure Coolify to pull from registry instead of building from source — switched Coolify to Docker Image build pack (not Dockerfile); Coolify now pulls ghcr.io/ioncernenchii/itinera:latest instead of building from source on droplet
  • Deleted old Nixpacks/Dockerfile-based Coolify service; new Docker Image service container label: wgg0oso8w00sgoco048w8k0s
  • Updated cron jobs on droplet to use new container label in docker inspect commands
  • Updated COOLIFY_WEBHOOK_URL GitHub secret with new service webhook URL
  • Remove swap space (/swapfile) — kept — 4GB droplet still benefits from swap headroom with 10+ services running
  • Outcome: All builds on GitHub Actions (7GB runner), zero builds on droplet, near-instant deploys via image pull

Next.js 16 Sentry Compatibility & ESLint Fixes

  • next.config.js: replace deprecated disableLogger: true with bundleSizeOptimizations: { excludeDebugStatements: true }
  • src/instrumentation-client.ts: add export const onRouterTransitionStart = Sentry.captureRouterTransitionStart
  • src/middleware.tssrc/proxy.ts rename per Next.js 16 requirement; function export renamed to proxy
  • ESLint fixes: add .eslintignore for Prisma generated code; wrap stateful callbacks in useCallback() with correct dependencies
  • All ESLint warnings resolved; app lints cleanly
  • Outcome: Phase 2 dependency work complete and production-ready

nuqs (URL-persistent filters) ✅ Phase 2 Complete

  • Install nuqs v2.8.8
  • Add NuqsAdapter to root layout wrapping children
  • Routes list page: 6 filters (q, driver, truck, status, dateFrom, dateTo) sync to URL with history: "replace"
  • Reports page: 7 filters (report, dateFrom, dateTo, status, action, userId, truckId) sync to URL with history: "replace"
  • Shared filtered URLs restore full filter state on page load
  • Tested in production — URL sharing works correctly

Phase 3 — Schema Foundations ✅ Complete

Add all schema needed by HERE migration, permits, and analytics. Run migrations before any new feature work.

Route enrichment fields:

  • Add total_distance_miles (Decimal 8,2 nullable) and toll_cost_estimated (Decimal 8,2 nullable) to routes — populated by HERE Routing API in Phase 4

Vehicle profiles (standalone lookup — no FK from routes):

  • Add vehicle_profiles table: id, samsara_vehicle_id (String, unique), height_inches (Int nullable), width_inches (Int nullable), length_feet (Int nullable), weight_lbs (Int nullable), axle_count (Int nullable), hazmat (Boolean default false), equipment_type (String nullable), created_at, updated_at
  • Note: routes.vehicle_id stays as a loose Samsara ID string — join to vehicle_profiles via samsara_vehicle_id when truck dimensions are needed (HERE routing). FK normalization deferred to Phase 8 multitenancy.

Permits (placeholder — schema TBD):

  • Reserve permits table in roadmap (deferred — exact fields to be designed when working with state DOT permit systems in Phase 6. Will include at minimum: route_id FK, state, permit_number, document_key for Minio. Placeholder table creation deferred to avoid schema churn.)

Notification improvements:

  • Add vehicle_id (String nullable) to notifications — currently fuel alerts reference vehicles only via JSON metadata; explicit column improves querying and deduplication
  • Add index on notifications.type — cron queries by type every 30 seconds for duplicate prevention
  • Add index on notifications.resolved_at — cron checks resolved status on every run
  • Backfill vehicle_id from existing metadata.vehicleId JSON for FUEL_LOW notifications (3 existing FUEL_LOW records updated)

Route indexes (performance):

  • Add composite index (status, created_at) on routes — dashboard sorts active routes by date
  • Add index on routes.vehicle_id — fleet queries filter by vehicle
  • Add index on routes.driver_id — assignment queries filter by driver
  • Add index on routes.status — dashboard and reports filter by status constantly

User ↔ Samsara linking:

  • Add samsara_driver_id (String nullable) and samsara_vehicle_id (String nullable) to users — needed for DRIVER/OWNER_OPERATOR scoping in Phase 9, cheap to add now

PostGIS extension:

  • Enable PostGIS: CREATE EXTENSION IF NOT EXISTS postgis; — needed for bridge/tunnel spatial queries (Phase 4) and state mileage calculations (Phase 5). Zero cost if unused, built into PostgreSQL 18.
  • Document in DATABASE_SCHEMA.md
  • Note: Manual .so file copy required since postgres:18-alpine doesn't bundle PostGIS; added initialization step in Coolify environment. Consider switching to postgis/postgis image in Phase 8 to avoid manual copy.

Migration execution:

  • Write and test all migrations locally (npx prisma migrate dev)
  • Deploy migrations to production via Coolify (npx prisma migrate deploy)
  • Update DATABASE_SCHEMA.md with new tables, indexes, and ER diagram
  • Update PLATFORM_SUMMARY.md schema section

Phase 4 — HERE Migration

Replace Google Maps with HERE Technologies for truck-native routing. Prerequisites: Phases 2 and 3 complete. Get HERE SDK Navigate Edition pricing quote before starting Phase 4c.

Phase 4a — Backend & APIs

  • HERE developer account, get API keys
  • Replace Google Directions with HERE Routing API v8 (truck mode + vehicle dimensions from vehicle_profiles)
  • Persist toll cost and route distance from HERE response to routes
  • Integrate HERE Fuel Prices API
  • Integrate HERE Geocoding & Search (replace Google Geocoding)
  • State mileage calculation: HERE polyline + Census Bureau state boundary GeoJSON → route_state_mileage.miles_estimated
  • NHPN/NN geometry pipeline from FHWA data
  • If PostGIS: sync bridge inventory (624K records) + tunnel inventory (~500 records) to local DB
  • Bridge/tunnel compliance checker: bounding box query, flag clearance conflicts

Phase 4b — Dispatcher Dashboard

  • Replace @react-google-maps/api with HERE Maps API for JavaScript
  • Drag-to-edit waypoints with truck routing recalculation
  • Truck restriction layer toggle on map
  • Fuel stop markers using HERE Fuel Prices API
  • Vehicle dimension profile selector on route creation
  • NN/NHFN "permit needed?" color overlay on route segments
  • Remove all Google Maps dependencies
  • Remove NEXT_PUBLIC_GOOGLE_MAPS_API_KEY from Coolify and Infisical
  • Add cmdk command palette

Phase 4c — Driver Mobile App (gated on HERE Navigate SDK pricing)

  • Contact HERE sales — get Navigate Edition quote for expected fleet size
  • Decision gate: proceed only if pricing fits business model
  • Build React Native (or native) app shell
  • Integrate HERE SDK Navigate Edition
  • Route loading from Itinera backend
  • Push notifications via Firebase FCM / APNs
  • Background GPS tracking (feeds route_state_mileage.miles_actual)
  • Driver login (scoped to DRIVER role)
  • TestFlight / Play Store beta

Phase 5 — Fuel Card Integration + Real Analytics

Replace mock data with real fuel transaction data.

  • Compass CSV importer → fuel_transactions table
  • Pilot Flying J SFTP automated nightly pull → parse → insert
  • Research Comdata/EFS/WEX API access
  • Backfill fuel_transactions with historical data
  • Wire /analytics and /analytics/[truckId] to real data
  • Build Financial Reports: Fuel Spend Report, Cost Per Mile
  • Populate route_state_mileage.miles_actual from Samsara GPS breadcrumbs on route completion
  • Build IFTA state mileage report
  • EIA regional diesel price API as fallback

Phase 6 — Minio (File Storage)

Move all file storage off the droplet filesystem. Prerequisite: Phase 3 (permits table has document_key waiting).

  • Deploy Minio via Coolify using Docker Compose at storage.getitinera.com
    • Domain set to http://storage.getitinera.com (Pattern B — no Let's Encrypt, Cloudflare handles TLS)
    • Tunnel config: storage.getitinera.comhttp://localhost:80
    • Persistent volume for data (critical — must survive container restarts)
    • Expose both API port (9000) and console port (9001) via Traefik
  • Create itinera bucket via Minio console; create dedicated access key (not root credentials)
  • Set bucket policy: private (presigned URLs for access, not public)
  • Install @aws-sdk/client-s3 in app
  • Create src/lib/storage.ts (uploadFile, getSignedUrl, deleteFile) pointed at Minio endpoint
  • Migrate fuel price Excel uploads from local filesystem to Minio
  • Add permit document upload (populate permits.document_key)
  • Add route export PDF storage (reports → Minio → shareable presigned link)
  • Bucket lifecycle policy: auto-delete temp files after 30 days
  • Keep Cloudflare R2 for database backups (off-site, separate from droplet — better for disaster recovery)
  • Test upload, retrieval, deletion end-to-end

Phase 7 — Polish, Documentation & Workflow Automation

Automate carrier-facing tasks. All workflows need real data — don't build against empty tables. Prerequisites: Phases 5 and 6 complete.

Deferred from Phase 1 (nice-to-have, not blocking)

Docusaurus (User Documentation)

  • Create Docusaurus project in /docs-site directory in the repo
  • Apply Itinera dark theme — match colors, fonts, and branding from the app
  • Write initial content: dispatcher user guide, route creation walkthrough, fuel alerts explanation
  • Deploy to Cloudflare Pages at docs.getitinera.com (same as homepage — auto-deploys from GitHub)
  • Add DNS CNAME in Cloudflare for docs.getitinera.com
  • Wire semantic-release changelog output into Docusaurus — auto-published release notes on each deploy

Transactional Email (Resend)

  • Create Resend account, verify getitinera.com domain
  • Install resend, create src/lib/email.ts (sendInviteEmail, sendPasswordResetEmail)
  • Wire invite generation to send email when email lock is set
  • Test invite email delivery end-to-end

Pino Structured Logging

  • Install pino and pino-pretty
  • Create src/lib/logger.ts singleton
  • Replace console.log/error across API routes and lib files
  • Include requestId in all log entries (generate per-request in middleware)
  • Configure Coolify to capture pino JSON output

Infisical — Full Secrets Audit

  • Audit all env vars currently set in Coolify across all services (Itinera app, Bugsink, Uptime Kuma)
  • Add every variable to Infisical production environment as source of truth
  • Document which variables are buildtime vs runtime only
  • Confirm Infisical UI reflects all current production secrets

n8n Workflow Automation

  • Deploy n8n via Coolify at workflows.getitinera.com
  • Connect n8n to PostgreSQL (read-only)
  • Build: carrier onboarding (welcome email via Resend → Telegram → spreadsheet)
  • Build: weekly fuel savings summary email (real fuel_transactions data)
  • Build: daily route completion digest to you via Telegram
  • Build: Samsara webhook handler (vehicle events → Itinera notifications)
  • Build: permit expiry reminder (query permits table → Telegram to dispatcher)
  • Test each workflow with real data before enabling
  • Add pgBackRest (replaces pg_dump script — DB is now large enough to warrant it)

Phase 8 — Multitenancy + Better Auth

Support multiple carrier companies with fully isolated data. Prerequisite: Phases 1–7 complete. Only start when a second carrier is ready to onboard.

Routing decision: Session/cookie-based (no URL change). Tenant is stored in auth session via organizationId. Users select org after login; all API queries scope to that tenantId. No subdomain or path-based routing needed initially. Subdomains (carrier.getitinera.com) can be added later as a middleware layer without restructuring — the hard work is data isolation, not URL routing.

Auth Migration

  • Audit current auth (TOTP, invites, RBAC, rate limiting, CSRF — document everything)
  • Install Better Auth + plugins (organization, twoFactor, admin)
  • Configure Better Auth with Prisma adapter; run CLI, review migrations
  • Add Company model (name, slug, subscription tier, settings)
  • Add company_id FK to: users, routes, invites, notifications, activity_logs, fuel_stops, fuel_transactions, route_documents, permits, vehicle_profiles, route_state_mileage
  • Migrate users to Better Auth format (preserve passwords, 2FA secrets, recovery codes)
  • Migrate invites to Better Auth organization invitations
  • Replace custom TOTP with Better Auth twoFactor plugin
  • Replace custom RBAC with Better Auth admin plugin
  • Update all API routes: Better Auth session + companyId scoping on every query
  • Add companyId to JWT (no extra DB lookup per request)
  • Add OWNER_OPERATOR role + fuel_pricing_rules table + markup logic
  • Deploy to staging first (via Coolify); parallel auth in prod briefly; remove NextAuth

Superadmin Layout + Org Switcher

  • Create superadmin layout (/admin) — cross-org dashboard, platform-wide analytics, global settings, carrier management
  • Build org switcher component in header — dropdown to select active organization, sets organizationId in session/context
  • Org-scoped pages: existing dashboard, routes, fleet, analytics, reports, and settings all filter by selected org's tenantId
  • Regular carrier users see only their own org (no switcher visible); superadmin sees all orgs + switcher
  • Superadmin dashboard: aggregate metrics across all carriers (total routes, total vehicles, revenue, active users)
  • Superadmin settings: manage companies (create, suspend, configure), manage all users, platform billing
  • Update Settings page: company profile, per-org member management (visible to org admins)

Testing & Validation

  • Test full auth flow: login, 2FA, invite, role check, company data isolation
  • Add Playwright tests: confirm Company A cannot access Company B data
  • Test org switcher: superadmin switches org → all pages reflect selected org's data
  • Test regular user: no access to other orgs, no switcher visible

Future: Subdomain Routing (optional, post-Phase 8)

  • Add Next.js middleware to extract subdomain → map to organizationId
  • Configure wildcard DNS *.getitinera.com → droplet IP
  • Configure Coolify/Traefik wildcard SSL cert via Let's Encrypt DNS challenge
  • Each carrier accesses via carrier-name.getitinera.com (rewrites to same app, sets org context)

Phase 9 — Driver Portal + DRIVER Role

Full driver-facing experience. Prerequisite: Phase 8 complete (drivers belong to specific companies).

  • Implement DRIVER role in Better Auth RBAC
  • Add samsara_driver_id linking on user record (populated on invite acceptance)
  • Build simplified driver portal: Profile, My Routes, My Analytics, Notifications
  • Scope all driver API responses to own assignments only
  • Integrate HERE SDK Navigate Edition into mobile app (from Phase 4c)
  • Push notifications for new route assignments
  • Driver GPS breadcrumbs → route_state_mileage.miles_actual in real time
  • Beta test with real drivers on TestFlight / Play Store internal track

Planned / In Progress

FeatureStatusNotes
Cloudflare DNS + Proxy + WAF✅ DonePhase 0 complete
Cloudflare R2 backups✅ DonePhase 0 complete
Security headers via Cloudflare Transform Rules✅ DonePending curl verify (DNS propagation)
Replace xlsx with exceljs✅ DonePhase 0 complete
Cloudflare Pages (static homepage)✅ DonePhase 1 complete
Coolify migration✅ Done4GB droplet, Infisical deployed, env vars in Coolify
Cloudflare Tunnel⏳ Phase 1Replaces Nginx reverse proxy, closes ports 80/443
Persistent rate limiting✅ DoneRaw Prisma (rate_limits table)
SUPERADMIN / READONLY roles✅ DonePurple/blue/grey badges, privilege escalation prevention
Vitest + Husky/lint-staged✅ DonePhase 2 testing foundation complete
Dependency upgrades batch 1✅ DoneTypeScript 5.9.3, React 19, Next.js 16.1.6, ESLint 9 flat config
Dependency upgrades batch 2✅ DonePrisma 5 → 7.4.1 with driver adapters, DATABASE_URL in prisma.config.ts
Docker build pipeline + disk cleanup✅ DoneGitHub Actions builds → ghcr.io; Coolify Docker Image build pack pulls :latest from ghcr.io (zero builds on droplet, near-instant deploys); Coolify webhook triggers on image push; disk freed 22GB
nuqs URL state persistence✅ DoneRoutes (6 filters) and reports (7 filters) sync to URL; shared URLs preserve state
Schema foundations⏳ Phase 3permits, vehicle_profiles, route_state_mileage
HERE Maps migration⏳ Phase 4See HERE_MIGRATION_PLAN.md
Fuel card integration🔄 ResearchingCompass CSV + Pilot SFTP access confirmed
Real analytics⏳ Phase 5Depends on fuel card integration
Minio file storage⏳ Phase 6
n8n automation⏳ Phase 7
Multitenancy + Better Auth⏳ Phase 8Only when second carrier is real
Driver mobile app⏳ Phase 4c/9HERE Navigate SDK pricing TBD

Future / No Fixed Phase

  • Dev + QA environments — separate Coolify apps + databases for dev.getitinera.com and qa.getitinera.com pointing to develop and qa branches. Makes sense when there's a team or paying customers at risk. Consider alongside 8GB droplet upgrade.