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.comto personal inbox (Phase 0) - Tunnel — replaces Nginx as reverse proxy, closes all inbound ports (Phase 1)
- Pages — hosts static
getitinera.comhomepage, 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
| Phase | Name | Status | Gate |
|---|---|---|---|
| 0 | Fix Live Risks | ✅ Complete | Do today — no prerequisites |
| 1 | Operational Stability | ✅ Complete | Before onboarding any real carriers |
| 2 | Dependency Upgrades + Testing | ✅ Complete | Before HERE migration |
| 3 | Schema Foundations | ✅ Complete | HERE migration ready |
| 4 | HERE Migration | ⏳ Phase 4 | After Phases 2 & 3 |
| 5 | Fuel Card Integration + Real Analytics | ⏳ Phase 5 | After HERE |
| 6 | Minio — File Storage | ⏳ Phase 6 | After Phase 3 (permits schema) |
| 7 | n8n — Workflow Automation | ⏳ Phase 7 | After Phases 5 & 6 (real data) |
| 8 | Multitenancy + Better Auth | ⏳ Phase 8 | Only when second carrier is ready |
| 9 | Driver Portal + DRIVER Role | ⏳ Phase 9 | After 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.comto 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.comloads 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
rcloneon 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.shto upload all.sql.gzfiles to R2 after each dump - Run backup script manually — file appears in R2 bucket confirmed
- Test restore:
gunzip -ton 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/*andapp.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: DENYX-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-originPermissions-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:3000instead ofhttps://app.getitinera.com - Verified:
curlfrom external returns 403, localhost returns{"success":true}
Replace xlsx with exceljs
- Uninstalled
xlsx, installedexceljs - Rewrote fuel price upload parsing in
/api/fuel-prices/route.tsusing ExcelJS - Rewrote report export in
report-utils.ts—toXLSXandfileResponsenow async - Updated all four
/api/reports/*routes toawait 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=devstep 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(skipped — not enforced on free private repos; revisit in Phase 8 when team grows)master
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-homepageconnected toioncernenchii/itinera, build outputhomepage, no build command - Set custom domains:
getitinera.comandwww.getitinera.com— both Active + SSL enabled - Verified
getitinera.comloads correctly from Cloudflare Pages - Removed homepage sync step from GitHub Actions deploy workflow
- Removed
/etc/nginx/sites-available/getitineraand/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.comusing 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 = truefor 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
productionenvironment — source of truth - Create machine identity
coolifywith Universal Auth credentials -
Connect Infisical to CoolifyDeferred 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 inspectwithcoolify.namelabel filter — survives redeploys - Both endpoints confirmed firing every 60s: 106 vehicles, 6 active routes, fuel + deviation alerts working
- Logs:
/var/log/cron-deviations.logand/var/log/cron-fuel.log
Cloudflare Tunnel ✅
- Install
cloudflaredon 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) withnoTLSVerify: true - Updated DNS CNAMEs in Cloudflare to point to
efa99bf0-1a82-43ed-8b32-06e454745179.cfargotunnel.com - Run
cloudflaredas 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.61times 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
coolifynetwork (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.com→http://localhost:80(not 443) — Uptime Kuma only has HTTP route - Key lesson:
/etc/cloudflared/config.ymlis what the systemd service reads — NOT/root/.cloudflared/config.yml; deleted the root copy to avoid confusion
- Docker Compose service with
- 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/samsaraendpoints - TCP port monitor not viable — DB container on isolated network, not reachable from Uptime Kuma
- Created
- Configure Telegram notification channel —
Itinera Infrastructuregroup (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/nextjsin app pointed at Bugsink DSN - Verify errors appear in dashboard
- Migrate Sentry config to
instrumentation.tspattern - Configure Telegram webhook for critical errors
- Fix Bugsink BASE_URL env var to
https://errors.getitinera.com - Delete
src/app/api/sentry-test/route.tstest endpoint
Persistent Rate Limiting ✅
-
Install— used raw Prisma instead (no new deps, consistent with codebase)rate-limiter-flexible - Replace
src/lib/rate-limit.tsin-memory store with PostgreSQL backing (rate_limitstable) - Run
npx prisma migrate deployon 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
SUPERADMINandREADONLYtoUserRoleenum inschema.prisma - Run Prisma migration (
20260226000001_add_superadmin_readonly_roles) - Update
requireAuth()and all API route role checks (newrequireSuperAdmin(),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
vitestand@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-releaseand plugins:@semantic-release/commit-analyzer,@semantic-release/release-notes-generator,@semantic-release/changelog,@semantic-release/git,@semantic-release/github - Add
.releaserc.jsonconfig — bump patch onfix:, minor onfeat:, major on breaking changes; skipchore:,docs:,refactor: - Add GitHub Actions
release.ymlworkflow: runssemantic-releaseon push tomaster - Verify: merging a
feat:commit auto-bumpspackage.json, generatesCHANGELOG.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-adapterif confirmed unused - Remove
uuidpackage if confirmed unused - Consolidate validation: Zod everywhere OR keep
lib/validation.ts— not both ✅ kept lib/validation.ts - Add GitHub Actions caching for
node_modulesand.nextbuild cache- Remove
continue-on-errorfromnpm auditstep indeploy.yml(was set as workaround for Next.js 14 CVEs — now fixed with Next.js 16 upgrade)
- Remove
Build Optimization (GitHub Actions → Docker → Coolify) ✅
- Write a
Dockerfilefor the Itinera app (multi-stage: deps/builder/runner, Node 22 Alpine base, explicitnpx 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:packagesscope in/root/.docker/config.json - Created
docs/GHCR_COOLIFY_DEPLOY.mdwith 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 -afto 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:latestinstead 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 inspectcommands - Updated
COOLIFY_WEBHOOK_URLGitHub secret with new service webhook URL -
Remove swap space (— kept — 4GB droplet still benefits from swap headroom with 10+ services running/swapfile) - 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 deprecateddisableLogger: truewithbundleSizeOptimizations: { excludeDebugStatements: true } -
src/instrumentation-client.ts: addexport const onRouterTransitionStart = Sentry.captureRouterTransitionStart -
src/middleware.ts→src/proxy.tsrename per Next.js 16 requirement; function export renamed toproxy - ESLint fixes: add
.eslintignorefor Prisma generated code; wrap stateful callbacks inuseCallback()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
nuqsv2.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) andtoll_cost_estimated(Decimal 8,2 nullable) toroutes— populated by HERE Routing API in Phase 4
Vehicle profiles (standalone lookup — no FK from routes): ✅
- Add
vehicle_profilestable: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_idstays as a loose Samsara ID string — join tovehicle_profilesviasamsara_vehicle_idwhen truck dimensions are needed (HERE routing). FK normalization deferred to Phase 8 multitenancy.
Permits (placeholder — schema TBD):
-
Reserve(deferred — exact fields to be designed when working with state DOT permit systems in Phase 6. Will include at minimum:permitstable in roadmaproute_idFK,state,permit_number,document_keyfor Minio. Placeholder table creation deferred to avoid schema churn.)
Notification improvements: ✅
- Add
vehicle_id(String nullable) tonotifications— 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_idfrom existingmetadata.vehicleIdJSON for FUEL_LOW notifications (3 existing FUEL_LOW records updated)
Route indexes (performance): ✅
- Add composite index
(status, created_at)onroutes— 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) andsamsara_vehicle_id(String nullable) tousers— 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
.sofile 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.mdwith new tables, indexes, and ER diagram - Update
PLATFORM_SUMMARY.mdschema 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/apiwith 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_KEYfrom Coolify and Infisical - Add
cmdkcommand 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_transactionstable - Pilot Flying J SFTP automated nightly pull → parse → insert
- Research Comdata/EFS/WEX API access
- Backfill
fuel_transactionswith historical data - Wire
/analyticsand/analytics/[truckId]to real data - Build Financial Reports: Fuel Spend Report, Cost Per Mile
- Populate
route_state_mileage.miles_actualfrom 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.com→http://localhost:80 - Persistent volume for data (critical — must survive container restarts)
- Expose both API port (9000) and console port (9001) via Traefik
- Domain set to
- Create
itinerabucket via Minio console; create dedicated access key (not root credentials) - Set bucket policy: private (presigned URLs for access, not public)
- Install
@aws-sdk/client-s3in 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-sitedirectory 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-releasechangelog output into Docusaurus — auto-published release notes on each deploy
Transactional Email (Resend)
- Create Resend account, verify
getitinera.comdomain - Install
resend, createsrc/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
pinoandpino-pretty - Create
src/lib/logger.tssingleton - Replace
console.log/erroracross API routes and lib files - Include
requestIdin 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
productionenvironment as source of truth - Document which variables are
buildtimevsruntimeonly - 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_transactionsdata) - Build: daily route completion digest to you via Telegram
- Build: Samsara webhook handler (vehicle events → Itinera notifications)
- Build: permit expiry reminder (query
permitstable → 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
Companymodel (name, slug, subscription tier, settings) - Add
company_idFK 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
twoFactorplugin - Replace custom RBAC with Better Auth
adminplugin - Update all API routes: Better Auth session +
companyIdscoping on every query - Add
companyIdto JWT (no extra DB lookup per request) - Add
OWNER_OPERATORrole +fuel_pricing_rulestable + 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
organizationIdin 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
DRIVERrole in Better Auth RBAC - Add
samsara_driver_idlinking 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_actualin real time - Beta test with real drivers on TestFlight / Play Store internal track
Planned / In Progress
| Feature | Status | Notes |
|---|---|---|
| Cloudflare DNS + Proxy + WAF | ✅ Done | Phase 0 complete |
| Cloudflare R2 backups | ✅ Done | Phase 0 complete |
| Security headers via Cloudflare Transform Rules | ✅ Done | Pending curl verify (DNS propagation) |
Replace xlsx with exceljs | ✅ Done | Phase 0 complete |
| Cloudflare Pages (static homepage) | ✅ Done | Phase 1 complete |
| Coolify migration | ✅ Done | 4GB droplet, Infisical deployed, env vars in Coolify |
| Cloudflare Tunnel | ⏳ Phase 1 | Replaces Nginx reverse proxy, closes ports 80/443 |
| Persistent rate limiting | ✅ Done | Raw Prisma (rate_limits table) |
| SUPERADMIN / READONLY roles | ✅ Done | Purple/blue/grey badges, privilege escalation prevention |
| Vitest + Husky/lint-staged | ✅ Done | Phase 2 testing foundation complete |
| Dependency upgrades batch 1 | ✅ Done | TypeScript 5.9.3, React 19, Next.js 16.1.6, ESLint 9 flat config |
| Dependency upgrades batch 2 | ✅ Done | Prisma 5 → 7.4.1 with driver adapters, DATABASE_URL in prisma.config.ts |
| Docker build pipeline + disk cleanup | ✅ Done | GitHub 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 | ✅ Done | Routes (6 filters) and reports (7 filters) sync to URL; shared URLs preserve state |
| Schema foundations | ⏳ Phase 3 | permits, vehicle_profiles, route_state_mileage |
| HERE Maps migration | ⏳ Phase 4 | See HERE_MIGRATION_PLAN.md |
| Fuel card integration | 🔄 Researching | Compass CSV + Pilot SFTP access confirmed |
| Real analytics | ⏳ Phase 5 | Depends on fuel card integration |
| Minio file storage | ⏳ Phase 6 | |
| n8n automation | ⏳ Phase 7 | |
| Multitenancy + Better Auth | ⏳ Phase 8 | Only when second carrier is real |
| Driver mobile app | ⏳ Phase 4c/9 | HERE Navigate SDK pricing TBD |
Future / No Fixed Phase
- Dev + QA environments — separate Coolify apps + databases for
dev.getitinera.comandqa.getitinera.compointing todevelopandqabranches. Makes sense when there's a team or paying customers at risk. Consider alongside 8GB droplet upgrade.