API Server

The central data API for all Travel Tamers applications. Built with Hono 4.12 on Node.js, backed by PostgreSQL via Drizzle ORM. Serves the marketing site, Nexus Ops Hub, Groups app, and n8n automation workflows through a unified REST interface with 32 route modules.

Architecture Overview

The API server is the data backbone of the Travel Tamers platform. It manages the primary tt_fresh_db PostgreSQL database (52 tables) and exposes REST endpoints for all CRUD operations across contacts, companies, deals, bookings, trips, payments, analytics, and more.

The server follows a layered architecture: Hono routes handle HTTP, Zod schemas validate all inputs, Drizzle ORM provides type-safe database queries, and custom error classes ensure consistent JSON error responses. External service integrations (Slack, Gmail, Claude AI, Buffer, Unsplash) are all optional and gracefully degrade to console logging when API keys are absent.

API health endpoint JSON response showing status ok with database ok and redis not_configured
Health endpoint — {"status":"ok","checks":{"database":"ok","redis":"not_configured"}}

Key Principle: All external services are optional in development. When API keys are not configured, the server logs what it would have done instead of failing. This means you can run the full API locally with just a PostgreSQL connection.

  • Internal service auth — new /api/internal/contacts and /api/internal/deals endpoints use x-internal-service header with timing-safe comparison instead of JWT. Replaces the broken Bearer token auth from TT-API to Nexus.
  • All 48 as any casts eliminated — across 21 API files. New isValidEnum() helper in src/utils/type-helpers.ts, Drizzle insert/update patterns use $inferInsert types, enum checks use proper type narrowing.
  • Webhook form-submit updated/webhooks/form-submit now receives honeypot field and a different payload shape (name instead of firstName+lastName). Both /webhooks/form-submit and /webhooks/page-view are now PUBLIC (skip requireServiceKey via PUBLIC_WEBHOOK_PATHS).
  • Nexus sync rewrittennexus/client.ts uses x-internal-service header instead of Bearer token. nexus/sync.ts calls /api/internal/contacts and /api/internal/deals.
  • Hono upgraded to 4.12.7 — 5 CVEs patched (serveStatic path traversal, setCookie injection, and others).
  • Webhook Zod validation — all 4 webhook endpoints (/webhooks/booking-update, /payment, /page-view, /miles-intent) now validate payloads with Zod schemas and return 400 on invalid input.
  • Booking payment ZodPOST /bookings/:id/payments validates with createBookingPaymentSchema.
  • Commission /calculate ZodPOST /commissions/calculate validates with calculateCommissionSchema (requires bookingId, vendorId, bookingValueCents).
  • Commission paginationGET /commissions now supports page/limit query params with pagination metadata in response.
  • Dockerfile production fixCMD uses node dist/index.js instead of tsx src/index.ts for faster startup and lower memory.
  • Health check hardened/health now verifies DB connectivity (SELECT 1) and Redis ping, returns 503 on failure.
  • Error middleware — stack traces stripped in production, only shown in development mode.

Request Flow

Request enters through Traefik

Traefik handles SSL termination and routes api.traveltamers.com to the Hono container on port 3100.

Middleware pipeline runs

Request ID, CORS, request logging, and global rate limiting are applied to every request.

Authentication checked

Routes under /api/* require a valid API key. Public routes (miles-chat, page-views, onboarding) skip auth.

Input validated with Zod

Request body and query parameters are parsed through Zod schemas. Invalid input returns 400 with details.

Database query via Drizzle

Type-safe queries are built with Drizzle ORM, supporting pagination, filtering, search, and soft deletes.

PostgreSQL 16

Tech Stack

Layer Technology Version Role
Framework Hono 4.12 HTTP server, routing, middleware (upgraded from 4.7 to patch 5 CVEs)
Runtime Node.js ≥20 Server runtime via @hono/node-server
Language TypeScript 5.7 Type safety across all layers
ORM Drizzle ORM 0.38 Type-safe SQL queries, migrations, schema
Database PostgreSQL 16 Primary data store (tt_fresh_db)
Cache Redis (ioredis) 7 Rate limiting, idempotency, caching
Validation Zod 3.24 Request body and query validation
AI Anthropic SDK 0.39 Claude-powered MilesChat
Slack @slack/web-api 7.8 Channel creation, notifications
Email Google APIs 144 Gmail API for transactional emails
Error Tracking Sentry 10.42 Unhandled error reporting
Testing Vitest 3.0 Unit and integration tests

Directory Structure

integrations/
├── src/
│   ├── index.ts              # Entry point: app setup, middleware, route mounting
│   ├── api/                  # 32 Hono route modules
│   │   ├── companies.ts
│   │   ├── contacts.ts
│   │   ├── deals.ts
│   │   ├── bookings.ts
│   │   ├── miles-chat.ts
│   │   └── ... (28 more)
│   ├── db/
│   │   ├── client.ts         # PostgreSQL connection (pg + drizzle)
│   │   ├── redis.ts          # Redis connection (ioredis)
│   │   ├── seed.ts           # Database seed script
│   │   └── schema/           # 12 Drizzle schema modules
│   │       ├── index.ts      # Re-exports all schemas
│   │       ├── enums.ts      # 27 pgEnum definitions
│   │       ├── companies.ts  # companies, contacts, deals, interactions
│   │       ├── bookings.ts   # bookings, travelers, documents, preferences
│   │       ├── trips.ts      # destinations, offerings, itineraries, client trips
│   │       ├── clients.ts    # clients, family groups, loyalty programs
│   │       ├── vendors.ts    # vendors, commission rules/entries
│   │       ├── financial.ts  # payments, invoices, schedules, touchpoints
│   │       ├── analytics.ts  # events, KPIs, automation runs, tasks
│   │       ├── marketing.ts  # proposals, emails, social media, assets
│   │       ├── groups.ts     # groups, members, milestones
│   │       └── content-management.ts
│   ├── middleware/
│   │   ├── auth.ts           # API key + service key auth
│   │   ├── rate-limit.ts     # Redis + in-memory rate limiting
│   │   ├── idempotency.ts    # Webhook deduplication
│   │   ├── request-id.ts     # Unique request ID header
│   │   └── slack-signature.ts # Slack HMAC-SHA256 verification
│   ├── utils/
│   │   ├── errors.ts         # Custom error classes
│   │   ├── logger.ts         # Structured logger
│   │   ├── validation.ts     # Shared Zod schemas
│   │   ├── type-helpers.ts   # isValidEnum() helper, type narrowing utilities (R11)
│   │   ├── cache.ts          # Redis cache helpers
│   │   ├── date.ts           # Date utilities
│   │   └── env.ts            # Environment helpers
│   ├── nexus/
│   │   ├── client.ts         # Nexus HTTP client (x-internal-service header, R11)
│   │   └── sync.ts           # Sync contacts/deals to Nexus via /api/internal/* (R11)
│   ├── webhooks/             # Webhook handler implementations
│   ├── slack/                # Slack command handlers
│   ├── email/                # Email sending utilities
│   ├── ai/                   # AI provider integrations
│   └── tasks/                # Background task runner
├── drizzle.config.ts
├── tsconfig.json
└── package.json

Database Schema

The tt_fresh_db database contains 52 tables organized into domain modules. All tables use UUIDs for primary keys, soft deletes via a deletedAt timestamp, and store monetary values in cents (integer) to avoid floating-point precision issues.

CRM & Sales

TableKey ColumnsPurpose
companies name, slug, stage, lead_score, annual_travel_budget, slack_channel_id Target companies in the sales pipeline
contacts first_name, last_name, email, company_id, lifecycle_stage, lead_score People associated with companies or standalone leads
contact_interactions contact_id, channel, direction, subject, body Communication history (email, Slack, phone, etc.)
deals company_id, contact_id, title, stage, amount, probability Sales opportunities tracked through pipeline stages
deal_stage_history deal_id, from_stage, to_stage, changed_at Audit trail of deal stage transitions
lead_scoring_rules name, criteria (JSONB), points, category Configurable rules for automated lead scoring

Clients & Travelers

TableKey ColumnsPurpose
clients client_type, status, lifetime_value_cents, travel_style, churn_risk_score Billing/booking entity with travel preferences
travelers first_name, last_name, date_of_birth, passport fields, TSA/Global Entry Individual traveler profiles with documents and medical info
travel_documents traveler_id, document_type, document_number, expiry_date Passports, visas, and other travel documents
traveler_preferences cabin_class, seat_preference, dietary, hotel/airline preferences Detailed travel preferences per traveler
client_addresses client_id, address_type, street, city, country Home, work, billing, and mailing addresses
emergency_contacts traveler_id, full_name, relationship, phone Emergency contacts for each traveler
family_groups name, group_type, primary_client_id Family, couple, or friend travel groups
loyalty_programs traveler_id, program_type, program_name, membership_number, tier Airline, hotel, cruise loyalty memberships

Trips & Bookings

TableKey ColumnsPurpose
destinations name, slug, country, region, continent, visa_required, safety_rating Travel destinations with metadata
trip_offerings name, trip_type, departure_date, price_from_cents, capacity Available trips from vendors (cruises, resorts, etc.)
itinerary_legs trip_offering_id, day_number, port_or_location Day-by-day itinerary for trip offerings
trips (client_trips) client_id, name, trip_type, status, start_date, total_cost_cents A specific client's booked trip
bookings deal_id, status, departure_date, destination, total_price_cents Individual booking records with vendor confirmations
booking_travelers booking_id, traveler_id, is_primary Join table: which travelers are on which booking

Financial

TableKey ColumnsPurpose
payments client_id, amount_cents, payment_type, status, transaction_id Payment records (deposits, installments, refunds)
payment_schedules trip_id, installment_number, amount_cents, due_date, status Scheduled payment installments
invoices invoice_number, total_cents, status, line_items (JSONB) Client invoices with itemized line items
commission_entries booking_id, vendor_id, gross_commission_cents, nexion_split_pct Commission tracking per booking
vendors name, tier, vendor_type, default_commission_pct Travel suppliers (cruise lines, hotels, etc.)
vendor_commission_rules vendor_id, trip_type, commission_pct, bonus_pct Per-vendor commission rate rules
vendor_contracts vendor_id, contract_type, commission_pct, cabins_held Group block agreements and rate contracts

Groups & Marketing

TableKey ColumnsPurpose
groups name, status, min/max_guests, departure_date, price_per_person_cents Group travel bookings (cruises, team offsites)
group_members group_id, contact_id, role, deposit_paid Members of each travel group
proposals company_id, slug, page_url, total_views, form_submissions Company-specific proposal pages
email_sequences name, trigger_event, is_active Automated email drip campaigns
email_sends contact_id, to_email, status, sent_at, opened_at Individual email send tracking
social_media_posts platform, body, scheduled_for, status, likes, impressions Social media content pipeline

Analytics & Operations

TableKey ColumnsPurpose
analytics_events event_name, session_id, page_url, utm_source, properties Website and app event tracking
kpi_snapshots snapshot_date, metric, value, change_percent Daily KPI snapshot records
automation_runs automation_id, status, execution_time_ms n8n workflow execution history
scheduled_tasks task_type, scheduled_for, status, retry_count Background task queue (emails, etc.)
touchpoints contact_id, channel, direction, subject, occurred_at All client touchpoints across channels
satisfaction_scores client_id, score_type, score, feedback, would_recommend NPS and satisfaction survey responses
travel_alerts destination_id, alert_type, severity, title Travel advisories and destination alerts
managed_content content_type, slug, title, body, status CMS-managed content for the marketing site
API authentication error showing UNAUTHORIZED code and API key is required message
Protected endpoint — authentication guard returns UNAUTHORIZED without API key

Enum Types

The schema uses PostgreSQL pgEnum types for constrained fields. These enforce valid values at the database level and provide TypeScript type safety through Drizzle.

EnumValues
company_stagetarget, researching, aware, engaged, qualified, proposal_sent, negotiating, won, active_client, churned, disqualified, archived
deal_stagelead, qualified, proposal_sent, proposal_viewed, negotiating, verbal_yes, booking_pending, booking_confirmed, traveling, completed, won, lost
booking_statusinquiry, quoted, hold, deposit_paid, confirmed, ticketed, in_progress, completed, cancelled, refunded
payment_statuspending, partial, paid, refunded, failed
commission_statusprojected, earned, invoiced, received, split_paid, final
trip_typeexpedition_cruise, river_cruise, ocean_cruise, group_trip, team_offsite, luxury_resort, adventure, cultural, event_based, custom
trip_statusinquiry, planning, quoted, booked, deposited, paid, traveling, completed, cancelled
group_statusplanning, interest_gauging, open, minimum_reached, closed, confirmed, traveling, completed, cancelled
client_typeindividual, family, corporate, group
client_statusprospect, active, vip, inactive, churned
vendor_tierpreferred, premium, standard
payment_typedeposit, installment, final_payment, supplement, refund
invoice_statusdraft, sent, viewed, paid, partial, overdue, void
schedule_statusupcoming, due, overdue, paid, waived
interaction_channellinkedin, email, slack, phone, video_call, in_person, website, buddy_chat, form_submission, newsletter_signup
lead_sourcebuiltin_list, linkedin_outreach, linkedin_inbound, website_form, buddy_chat, referral, event, cold_email, social_media, organic_search, other
touchpoint_channelslack, email, phone, video, text, linkedin, in_person, website, buddy_chat, mail
touchpoint_directioninbound, outbound
email_send_statusqueued, sent, delivered, opened, clicked, bounced, failed, unsubscribed
social_platformlinkedin, instagram, facebook, x
alert_severityinfo, warning, critical
content_statusdraft, published, archived
address_typehome, work, mailing, billing, vacation
family_group_typefamily, couple, friends, corporate_team
loyalty_program_typeairline, hotel, cruise, car_rental, rail, trusted_traveler, other
contact_rolec_suite, vp, director, hr_culture, office_manager, travel_coordinator, executive_assistant, individual, other
notification_channelslack, email, sms, push

Authenticated API Routes

All routes under /api/* require a valid API key (via X-API-Key header or Authorization: Bearer). Each route module follows a standard CRUD pattern with pagination, search, UUID validation on :id parameters, and Zod validation on request bodies.

Route PrefixEndpointsDescription
/api/companies GET list, detail • POST create • PATCH update • DELETE Company records with stage pipeline, lead scoring, Slack integration
/api/contacts GET list, detail • POST create • PATCH update • DELETE Contact profiles with lifecycle stages and engagement scoring
/api/deals GET list, detail • POST create • PATCH update • DELETE Sales pipeline deals with stage history tracking
/api/clients GET list, detail • POST create • PATCH update • DELETE Client billing entities with lifetime value and travel profiles
/api/travelers GET list, detail • POST create • PATCH update • DELETE Individual traveler profiles with documents and medical info
/api/bookings GET list, detail • POST create • PATCH update • DELETE Booking records with vendor confirmations and payment tracking. Sub-route POST /:id/payments validates with createBookingPaymentSchema (Zod).
/api/trips GET list, detail • POST create • PATCH update • DELETE Trip offerings (destinations, cruises, itineraries)
/api/client-trips GET list, detail • POST create • PATCH update • DELETE Client-specific trip records with dates, costs, itinerary items
/api/groups GET list, detail • POST create • PATCH update • DELETE Group travel bookings with member management and milestones
/api/payments GET list, detail • POST create • PATCH update Payment records (deposits, installments, refunds)
/api/payment-schedules GET list, detail • POST create • PATCH update Scheduled payment installments with reminder tracking
/api/invoices GET list, detail • POST create • PATCH update Client invoices with line items and delivery tracking
/api/commissions GET list, detail, summary • POST /calculate • PATCH update Commission entries with vendor splits and Nexion reconciliation. GET / supports page/limit pagination query params. POST /calculate validates with calculateCommissionSchema (Zod).
/api/vendors GET list, detail • POST create • PATCH update Travel supplier profiles with commission structures
/api/analytics GET events, KPIs, dashboard summaries Analytics data for the Nexus dashboard
/api/social GET list • POST create • PATCH update Social media post management and scheduling
/api/touchpoints GET list • POST create Client interaction touchpoints across all channels
/api/satisfaction GET list • POST create NPS scores and satisfaction survey responses
/api/travel-alerts GET list • POST create • PATCH update Travel advisories and destination alerts
/api/activities GET list • POST create Activity tracking and logging
/api/family-groups GET list, detail • POST create • PATCH update Family and friend travel groups
/api/loyalty-programs GET list • POST create • PATCH update Traveler loyalty program memberships
/api/client-addresses GET list • POST create • PATCH update • DELETE Client address management
/api/emergency-contacts GET list • POST create • PATCH update • DELETE Traveler emergency contacts
/api/content GET list, detail • POST create • PATCH update CMS-managed content (blog posts, page sections, testimonials)
/api/intelligence GET /top-opportunities AI-powered trip recommendations for n8n automation workflows

Public & Internal Routes

Some routes are public-facing (no API key required) and others are reserved for internal service-to-service communication.

Public Routes (No Auth)

RouteRate LimitDescription
GET /health Global (100/min) Deep health check: runs SELECT 1 against PostgreSQL and pings Redis. Returns 503 with failing check details if either is unreachable. Docker HEALTHCHECK uses this endpoint every 30s.
POST /api/miles-chat/* 20/min per IP AI-powered travel chat (Claude Sonnet). Validates with Zod, escapes HTML for XSS prevention.
POST /api/page-views/* 100/min per IP Anonymous page view tracking from the marketing site
POST /api/onboarding/* 100/min per IP New client onboarding form submissions
POST /slack/commands Global Slack slash command handler (verified via HMAC-SHA256 signature)
POST /slack/events Global Slack event subscription handler (URL verification + event dispatch)

Internal Service Routes

RouteAuthDescription
GET /api/internal/contacts x-internal-service Nexus → TT-API contact sync. Uses INTERNAL_SERVICE_SECRET with timing-safe comparison (not JWT). Supports query params for filtering.
GET /api/internal/deals x-internal-service Nexus → TT-API deal sync. Same x-internal-service header auth as contacts. Supports query params for filtering.
POST /internal/send-email Service Key Email sending for n8n workflows (supports templates and plain HTML)
POST /internal/* Service Key Internal service endpoints for workflow triggers and dead letter management
POST /webhooks/form-submit Public + Idempotency Form submissions from proposal pages (creates CRM records + Slack channels). Now receives a honeypot field for spam detection and a unified name field instead of firstName+lastName. Skips requireServiceKey via PUBLIC_WEBHOOK_PATHS.
POST /webhooks/booking-update Service Key + Idempotency Vendor booking confirmation/update. Zod-validated with bookingUpdateSchema. Returns 400 on invalid payload.
POST /webhooks/payment Service Key + Idempotency Payment notification handler. Zod-validated with paymentSchema (requires bookingId + amountCents).
POST /webhooks/page-view Public + Idempotency Proposal page view analytics. Zod-validated with pageViewSchema. Records to analytics_events. Public (skips requireServiceKey via PUBLIC_WEBHOOK_PATHS).
POST /webhooks/miles-intent Service Key + Idempotency High-intent detection from Miles chatbot. Zod-validated with milesIntentSchema. Records to analytics_events.
POST /hooks/groups/* GROUPS_WEBHOOK_SECRET Groups app sync webhooks (separate auth from main webhook system)

Idempotency: All /webhooks/* routes enforce idempotency via the X-Idempotency-Key header or an auto-generated SHA-256 hash of the request path + body. Duplicate deliveries within 24 hours return 409 Conflict. This prevents double-processing when n8n or external services retry webhook calls.

Authentication

The API uses a multi-layer authentication system depending on the route type. All secret comparisons use timing-safe equality checks to prevent timing attacks.

API Key Authentication

Routes under /api/* require a valid key sent via X-API-Key header or Authorization: Bearer <key>. Valid keys are configured in the API_KEYS environment variable (comma-separated list).

# Example request
curl -H "X-API-Key: your-key-here" \
  https://api.traveltamers.com/api/companies

Authentication Modes

ModeHeaderUsed By
API Key X-API-Key or Authorization: Bearer Nexus Ops Hub, external integrations
Service Key X-Webhook-Secret n8n workflows, internal webhook calls
Internal Service x-internal-service Nexus Ops Hub → TT-API (Docker network). Uses INTERNAL_SERVICE_SECRET env var with timing-safe comparison. Replaces the earlier broken Bearer token approach. Powers /api/internal/contacts and /api/internal/deals.
Slack Signature X-Slack-Signature + X-Slack-Request-Timestamp Slack commands and events (HMAC-SHA256)

Dev Mode: When API_KEYS is not set and NODE_ENV is not production, the auth middleware logs a warning but allows requests through. In production, missing keys always return 401 Unauthorized.

Rate Limiting

Rate limiting uses Redis INCR + EXPIRE for distributed counting across server instances. When Redis is unavailable, an in-memory Map provides fallback protection. Every response includes rate limit headers.

TierLimitWindowApplied To
Global 100 req 1 minute All routes (baseline protection)
General 100 req 1 minute /api/*, /api/page-views/*, /api/onboarding/*
MilesChat 20 req 1 minute /api/miles-chat/*
Webhooks 30 req 1 minute /webhooks/*, /hooks/groups/*
Auth 10 req 1 minute Authentication endpoints

Response Headers

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1710280320
Retry-After: 45        # Only when 429 is returned

Input Validation

Every endpoint validates inputs using Zod schemas. Query parameters, path parameters (UUID format), and request bodies are all parsed through Zod before reaching handler logic. Invalid input throws a ValidationError which returns a 400 response with details.

Shared Validation Schemas

Common schemas in src/utils/validation.ts are reused across all route modules:

SchemaValidates
uuidParam Path :id parameters are valid UUIDs (applied to all 23 route files)
listQuerySchema Pagination: page, limit, search, sortBy (with allowlist), sortOrder
createCompanySchema Company creation: required name, optional industry, stage, etc.
updateCompanySchema Partial company update (all fields optional)
createContactSchema Contact creation with email validation, lifecycle stage constraints
paginationSchema Reusable page/limit query params with defaults (used by commissions, bookings, etc.)
bookingUpdateSchema Webhook: booking update payload (requires bookingReference)
paymentSchema Webhook: payment notification (requires bookingId + amountCents)
pageViewSchema Webhook: page view analytics (sessionId, pageUrl, UTM params)
milesIntentSchema Webhook: Miles high-intent detection (sessionId, intentScore, messages)
createBookingPaymentSchema Booking sub-route: payment creation (requires amountCents + paymentMethod)
calculateCommissionSchema Commission calculation (requires bookingId, vendorId, bookingValueCents as UUID/positive int)

Pattern: Each route module imports shared schemas and may extend them with domain-specific fields. The sortBy parameter uses an allowlist of valid column names to prevent SQL injection through sort field manipulation.

Type Safety (R11)

All 48 as any type casts have been eliminated across 21 API files. The codebase now uses proper TypeScript patterns throughout:

PatternLocationDetails
isValidEnum() src/utils/type-helpers.ts Generic helper that checks if a value belongs to a Drizzle pgEnum's value set, returning a type-narrowed result. Replaces as any casts on enum comparisons.
$inferInsert All Drizzle insert/update calls Insert and update patterns now use Drizzle's $inferInsert types instead of as any on the data object.
Enum type narrowing Route handlers with enum filters Query parameter enum checks use isValidEnum() to narrow the type before passing to Drizzle's eq() function.

Error Handling

The API uses a hierarchy of custom error classes that extend AppError. The global error handler in app.onError() catches all exceptions and returns consistent JSON responses. Unhandled errors are reported to Sentry in production.

Error ClassHTTP StatusCodeWhen Used
ValidationError 400 VALIDATION_ERROR Invalid input from Zod parsing failures
AuthError 401 UNAUTHORIZED Missing or invalid API key
NotFoundError 404 NOT_FOUND Resource not found (entity + identifier in message)
ConflictError 409 CONFLICT Duplicate records, state conflicts, idempotency duplicates
RateLimitError 429 RATE_LIMITED Too many requests (includes retryAfterMs)
ExternalServiceError 502 EXTERNAL_SERVICE_ERROR Slack, Gmail, or other external API failure
AppError (base) 500 INTERNAL_ERROR Unhandled server errors (sanitized in production)

Error Response Format

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input",
    "details": [
      { "path": ["email"], "message": "Invalid email format" }
    ],
    "requestId": "req_abc123"
  }
}

Production Sanitization: In production, ValidationError details are stripped to avoid leaking schema information. Server errors (500+) return a generic message instead of the actual error. Stack traces are completely stripped in production and only included in development mode. Full details are always logged server-side and sent to Sentry.

Middleware Stack

Middleware runs in declaration order. The stack is designed so that request tracking and security layers run first, before any route-specific logic.

Request ID

Generates or forwards a unique X-Request-Id header for request tracing across services.

CORS

Configurable via CORS_ORIGINS env var. Allows GET, POST, PATCH, DELETE. Exposes X-Request-Id. Max age 3600s.

Request Logging

Logs method, path, status, and duration for every request at DEBUG level.

Global Rate Limit

100 req/min per IP baseline on all routes. Uses Redis with in-memory fallback.

Route-Specific Middleware

Additional rate limits, authentication (requireAuth / requireServiceKey), idempotency, and Slack signature verification.

FilePurpose
middleware/auth.ts requireAuth (API key), requireServiceKey (webhook secret), optionalAuth, internal service detection
middleware/rate-limit.ts Factory function + 5 pre-configured tiers (global, general, auth, webhook, miles-chat)
middleware/idempotency.ts Webhook deduplication via X-Idempotency-Key or SHA-256 hash. 24-hour TTL. Redis + memory fallback.
middleware/request-id.ts Generates UUID request IDs, forwards existing X-Request-Id headers
middleware/slack-signature.ts HMAC-SHA256 verification of Slack request signatures using SLACK_SIGNING_SECRET

Development

Commands

# From the integrations/ directory:

npm run dev          # tsx watch src/index.ts (hot-reload on file changes)
npm run build        # tsc (compile TypeScript to dist/)
npm run start        # node dist/index.js (run compiled output)
npm run test         # vitest run (execute test suite)
npm run test:watch   # vitest (watch mode)
npm run lint         # eslint src/
npm run typecheck    # tsc --noEmit (type checking without emit)

Database Commands

npm run db:push      # Apply schema directly to database (dev)
npm run db:generate  # Generate migration files from schema changes
npm run db:migrate   # Run pending migrations
npm run db:seed      # Seed the database with sample data
npm run db:studio    # Open Drizzle Studio (visual DB browser)
Groups app health endpoint JSON showing status ok app groups
Health check pattern — same JSON structure used across all services

Environment Variables

VariableRequiredPurpose
DATABASE_URLYesPostgreSQL connection string
API_KEYSProductionComma-separated valid API keys
WEBHOOK_SECRETProductionShared secret for webhook auth
REDIS_URLNoRedis connection (rate limiting, idempotency)
SLACK_BOT_TOKENNoSlack Bot for channels and messages
SLACK_SIGNING_SECRETNoSlack request signature verification
ANTHROPIC_API_KEYNoClaude API for MilesChat
GOOGLE_SERVICE_ACCOUNT_KEY_FILENoGmail API credentials
SENTRY_DSNNoSentry error tracking
CORS_ORIGINSNoAllowed CORS origins (comma-separated)
LOG_LEVELNoLogging level (debug, info, warn, error)
INTERNAL_SERVICE_SECRETNoInternal service-to-service auth
GROUPS_WEBHOOK_SECRETNoGroups app webhook auth
SLACK_ONBOARDING_WEBHOOKNoSlack webhook URL for onboarding notifications (R11)

Key Files

FilePurpose
src/index.ts Main entry: app setup, all middleware registration, route mounting, error handling, graceful shutdown
src/db/schema/index.ts Re-exports all 12 schema modules (enums, companies, trips, bookings, clients, etc.)
src/db/schema/enums.ts 27 pgEnum type definitions used across all tables
src/utils/errors.ts Custom error class hierarchy (AppError, ValidationError, NotFoundError, etc.)
src/utils/validation.ts Shared Zod schemas (uuidParam, listQuerySchema, create/update schemas)
src/utils/type-helpers.ts isValidEnum() helper and type narrowing utilities (R11, replaces all as any casts)
src/nexus/client.ts Nexus HTTP client using x-internal-service header (R11, replaces Bearer token)
src/nexus/sync.ts Syncs contacts and deals to Nexus via /api/internal/* endpoints (R11)
src/middleware/auth.ts API key auth, service key auth, internal service header detection
drizzle.config.ts Drizzle Kit configuration for migrations and studio

Deployment

The API server runs as a Docker container (tt-api) behind the Traefik reverse proxy. Deployment is automated through GitHub Actions with rollback support.

Deployment Pipeline

GitHub Actions triggered

Push to main branch or manual dispatch starts the deploy workflow.

GitHub Actions

TypeScript build + tests

npm run typecheck, npm run lint, npm run test, then npm run build compiles to dist/.

Archive transferred to VPS

Built code is sent to the VPS via git archive or tar over SSH.

SSH + SCP

Docker container rebuilt

The tt-api container is rebuilt and restarted using docker-compose.production.yml.

Docker Compose

Health check verification

A request to /health verifies database and Redis connectivity. On failure, the previous container image is restored.

Production Infrastructure

# Docker container: tt-api
# Host: api.traveltamers.com
# Port: 3100 (internal)
# Database: tt_fresh_db (PostgreSQL 16)
# Cache: Redis 7 (shared with other services)
#
# Traefik handles:
#   - SSL termination (Let's Encrypt)
#   - HTTP → HTTPS redirect
#   - Routing api.traveltamers.com → tt-api:3100
#
# Health monitoring:
#   - /health endpoint checked every 5 minutes
#   - Failures reported to Slack #monitoring channel

Graceful Shutdown

The server listens for SIGINT and SIGTERM signals. On shutdown, it stops the background task runner, closes the Redis connection, closes the PostgreSQL connection pool, and then exits. This ensures in-flight requests complete and connections are properly released.

Rollback Safety: Each deployment tags the current Docker image before rebuilding. If the health check fails, the deploy script automatically rolls back to the previous image. The previous dist/ directory is also preserved as dist-old/ for manual recovery.

Production Dockerfile: The production image uses a multi-stage build (Node 20 Alpine). The final CMD runs node dist/index.js (compiled JavaScript) instead of tsx src/index.ts for faster startup and lower memory usage. Source files are still included for migrations and seeding via tsx. The container runs as the node user (non-root) with a built-in HEALTHCHECK every 30 seconds.