Nexus Ops Hub
The unified command center for Travel Tamers. A full CRM with deals pipeline, marketing automation with email sequences and lead scoring, a service desk with ticketing and SLA tracking, travel operations management, and an operations hub that aggregates Slack, n8n, Google Workspace, and analytics into a single view. Deployed at shane.traveltamers.com.
Overview
Nexus is a monolithic full-stack application split into three runtime processes: a
Fastify 5 API server, a React 19 SPA served by nginx,
and a BullMQ worker process for background job execution. All three share
the same TypeScript codebase in the nexus/ directory, which is a git subrepo
with its own .git (no remote configured).
The API handles all business logic, authentication, and data access through
Drizzle ORM against a PostgreSQL 16 database (nexus_crm).
Redis 7 backs both the BullMQ job queues and an application cache layer.
Socket.io provides real-time push notifications to connected React clients
without polling.
Nexus covers six major domains: CRM (contacts, companies, deals, tasks, activities), Marketing (campaigns, sequences, segments, forms, landing pages, lead scoring), Sales (products, quotes, playbooks), Service Desk (tickets, knowledge base, CSAT surveys), Travel Operations (trips, bookings, travelers, suppliers, finance, itinerary builder), and an Operations Hub that aggregates external systems (Slack, n8n, Google Workspace, analytics) into a single view.
Tech Stack
Backend
| Technology | Version | Purpose |
|---|---|---|
| Fastify | 5.2 | HTTP framework with hooks, plugins, validation, Swagger docs |
| TypeScript | 5.7 | Strict mode, end-to-end type safety |
| Drizzle ORM | 0.38 | Type-safe schema definitions, queries, and migrations |
| BullMQ | 5.30 | Redis-backed job queues with retry, backoff, dead-letter handling |
| Socket.io | 4.8 | Real-time push notifications to connected clients |
| JWT + bcryptjs | — | Bearer token auth with refresh tokens and cookie sessions |
| Zod | 3.24 | Runtime input validation on all API endpoints |
| Pino | 9.6 | Structured JSON logging with configurable levels |
| Sentry | 10.42 | Error tracking and performance monitoring (optional) |
Frontend
| Technology | Purpose |
|---|---|
| React 19 + Vite | SPA with hot module replacement, lazy-loaded route code splitting |
| React Router | Client-side routing for 80+ page components |
| TanStack Table | Data tables with sorting, filtering, pagination |
| Zustand | Lightweight state management (auth store, notification store) |
| Tailwind CSS | Utility-first styling with Dark Sanctuary design tokens |
| @dnd-kit/core | Drag-and-drop for Kanban boards and workflow builders |
| Lucide React | Icon library across all UI components |
Infrastructure
| Technology | Purpose |
|---|---|
| PostgreSQL 16 | Primary database (nexus_crm) — 54 tables, 60 pgEnum types |
| Redis 7 | BullMQ job queues + application cache (256 MB, appendonly, noeviction) |
| Docker Compose | 5-container orchestration with health checks and memory limits |
| Traefik | Reverse proxy with automatic TLS (Let's Encrypt) and path-based routing |
| nginx | Static file serving for the React SPA with CSP headers |
Key Dependencies
@fastify/cors— Cross-origin configuration@fastify/helmet— Security headers@fastify/rate-limit— Request throttling@fastify/multipart— File uploads@fastify/swagger— API documentation
pdf-lib— PDF generation for quotes and itinerariesimapflow+mailparser— Inbound email processingdompurify+jsdom— HTML sanitizationioredis— Redis client for BullMQ and cachingdate-fns— Date manipulation utilities
Architecture
Nexus runs as 5 Docker containers orchestrated by Docker Compose. All containers have
memory limits, JSON log rotation (10 MB / 3 files), and health checks. Traefik handles
external TLS termination and path-based routing: /api/*,
/socket.io/*, /lp/*, /kb/*, /forms/*,
/unsubscribe/*, and /meet/* route to the API container;
everything else routes to the web container.
| Container | Image | Role | Port | Memory |
|---|---|---|---|---|
nexus-api | Node 20 (multi-stage) | Fastify API, Socket.io, JWT auth, all business logic | 3000 | 512 MB |
nexus-web | nginx:alpine | React SPA static files, client-side routing fallback, CSP headers | 80 | 256 MB |
nexus-worker | Node 20 (same image as api) | BullMQ job processor — 5 queue workers + scheduled campaign checker | — | 512 MB |
nexus-db | postgres:16-alpine | PostgreSQL database (nexus_crm) | 5432 | 1 GB |
nexus-redis | redis:7-alpine | BullMQ queues + application cache (appendonly, noeviction) | 6379 | 256 MB |
Network Topology
Three Docker networks isolate traffic:
- nexus-net (bridge) — Internal communication between all 5 Nexus containers
- proxy (external) — Shared with Traefik for TLS-terminated inbound traffic
- internal (external) — Shared with the TT-API container for the travel proxy
Named Volumes
nexus-db-data— PostgreSQL data directory (persistent)nexus-redis-data— Redis AOF persistenceuploads— File uploads shared between API and worker containers
Worker entrypoint: The worker container uses the same Docker image as the API but overrides the command to
node dist/workers/index.js. This means a single build produces both the API and worker artifacts.
Database
The nexus_crm database runs in a dedicated PostgreSQL 16 container
(nexus-db), separate from the main TT-API database. The schema is managed
by Drizzle ORM with TypeScript type generation. All tables use UUIDs for
primary keys (except ticket numbers which use SERIAL), soft deletes via
isDeleted + deletedAt, and monetary values in cents.
Key Statistics
Schema
54 Tables
- 16 schema module files
- 60 pgEnum type definitions
- 68 columns converted VARCHAR → pgEnum
- All enums use
nx_prefix
Features
Data Patterns
- UUID primary keys (SERIAL for tickets)
- Soft deletes on all domain tables
- JSONB custom fields on contacts, companies, deals
- TEXT[] arrays for flexible tagging
| File | Tables Covered |
|---|---|
enums.ts | 60 pgEnum definitions with nx_ prefix |
contacts.ts | Contacts, companies, activities, tasks |
companies.ts | Company records, account tiers, engagement levels |
deals.ts | Deals, pipeline stages, deal activities |
activities.ts | Activity timeline records |
tasks.ts | Assignable tasks with due dates and priorities |
marketing.ts | Campaigns, templates, sequences, segments, email sends |
automation.ts | Workflows, steps, enrollments, executions |
travel.ts | Trips, bookings, travelers, suppliers, documents |
system.ts | Users, sessions, audit log, notifications |
products.ts | Product catalog, pricing tiers |
quotes.ts | Quotes, line items, discounts |
surveys.ts | Surveys, questions, responses, NPS/CSAT scores |
ops.ts | Service health, social posts, enrichment data |
serviceHub.ts | Tickets, knowledge base articles, categories |
tracking.ts | Page views, email events, click tracking |
All enums are defined in nexus/src/db/schema/enums.ts. They enforce
type safety at the database level and replaced 68 VARCHAR columns through a manual
SQL migration (Drizzle cannot handle VARCHAR-to-pgEnum conversions).
| Category | Enum Types |
|---|---|
| Shared | nx_priority, nx_discount_type, nx_billing_frequency, nx_cabin_class |
| Contacts | nx_lifecycle_stage, nx_lead_status, nx_lead_source |
| Companies | nx_company_type, nx_company_status, nx_account_tier, nx_engagement_level |
| Deals | nx_pipeline_type, nx_deal_type, nx_deal_source |
| Activities | nx_activity_type, nx_activity_status |
| Tasks | nx_task_type, nx_task_status, nx_team_role |
| Tickets | nx_ticket_source, nx_ticket_category |
| Marketing | nx_email_template_type, nx_campaign_status, nx_campaign_type, nx_email_send_status, nx_bounce_type, nx_segment_type, nx_form_status, nx_form_type, nx_landing_page_status, nx_lead_scoring_rule_type |
| Automation | nx_workflow_status, nx_workflow_type, nx_trigger_type, nx_enrollment_status, nx_sequence_status |
| Travel | nx_supplier_type, nx_traveler_status, nx_seat_preference, nx_booking_type, nx_booking_status, nx_payment_status, nx_commission_status, nx_document_type |
| Service | nx_kb_article_status, nx_quote_status |
| Surveys | nx_survey_type, nx_survey_status, nx_sentiment |
| Ops | nx_service_health_status, nx_service_health_category, nx_social_platform, nx_social_post_status, nx_social_post_source, nx_enrichment_data_source |
| Slack | nx_slack_request_type, nx_slack_request_status |
| System | nx_audit_action, nx_notification_type, nx_email_event_type |
RBAC Role System
Nexus uses a 5-tier role-based access control system defined in
nexus/src/middleware/rbac.ts. Each role has a numeric rank, and access checks
compare the user's rank against the minimum required for the operation. Two guard functions
are provided: requireRole(min) for hierarchical checks and
requireAnyRole(...roles) for OR-logic checks. Unknown or missing roles
receive rank -1 and are always denied.
| Role | Rank | Access Level |
|---|---|---|
admin |
4 | Full system access. User management, queue administration, settings, audit log. Can impersonate other roles. |
manager |
3 | All CRM and marketing operations. Team oversight, reports, pipeline configuration. Cannot manage users or system settings. |
advisor |
2 | Create and edit contacts, deals, campaigns, workflows. Full travel operations access. Cannot modify pipelines or team settings. |
agent |
1 | Basic CRM operations. Create contacts, log activities, manage assigned tasks and tickets. Cannot create campaigns or workflows. |
viewer |
0 | Read-only access. View dashboards, reports, and records but cannot create, edit, or delete anything. |
Protected Route Groups
All 10 main route groups are protected by the RBAC middleware:
- Contacts & Companies
- Deals & Pipelines
- Marketing (campaigns, segments, forms)
- Automation (workflows, sequences)
- Travel Operations (trips, bookings)
- Service Hub (tickets, KB)
- Products & Quotes
- Reports & Analytics
- Integrations (n8n, Slack, Google)
- Settings & Team Management
JWT Authentication Flow
Login
User submits email + password. Server validates with bcryptjs, issues JWT access token (short-lived) and refresh token (long-lived httpOnly cookie).
Bearer Token
React SPA attaches the JWT as Authorization: Bearer <token> on every API call. Fastify preHandler hook decodes and validates.
Token Refresh
When the access token expires, the client calls the refresh endpoint with the httpOnly cookie. A new access token is issued silently.
RBAC Guard
After authentication, requireRole() checks the user's rank against the route minimum. Insufficient rank returns 403 Forbidden.
Rank inheritance: Routes specify a minimum rank (e.g.,
requireRole('advisor')means rank ≥ 2). Managers and admins automatically inherit all lower-tier permissions without explicit grants.
Auth Mechanisms
Nexus uses three distinct authentication mechanisms, each serving a different type of caller. All three are enforced via Fastify preHandler hooks and return appropriate HTTP status codes on failure.
| Mechanism | Middleware | Used By | Failure Codes |
|---|---|---|---|
| JWT (User Auth) | authenticate.ts |
React SPA, all standard API routes | 401 Unauthorized |
| Internal Service | serviceAuth.ts |
TT-API contact/deal sync | 503 if secret not set, 401 if mismatch |
| Slack Webhook | Inline HMAC-SHA256 | Slack event subscriptions | 503 if secret not set, 401 if invalid signature |
Internal Service Auth
The serviceAuth.ts middleware provides machine-to-machine authentication for
internal services (currently TT-API). It performs a timing-safe comparison of the
x-internal-service request header against the TT_INTERNAL_SERVICE_SECRET
environment variable. If the secret is not configured, the endpoint returns 503 Service
Unavailable rather than silently allowing unauthenticated access.
Internal service routes are registered in server.ts under the
/api/internal prefix. These endpoints allow TT-API to sync contacts and
deals into the Nexus CRM without requiring JWT authentication.
| Endpoint | Method | Purpose |
|---|---|---|
/api/internal/contacts | POST | Sync contacts from TT-API into Nexus CRM |
/api/internal/deals | POST | Sync deals from TT-API into Nexus CRM |
Secret required: The
TT_INTERNAL_SERVICE_SECRETmust be set in the production.envfile and must match the value configured in TT-API. Without it, all internal service endpoints return 503.
API Routes
The Fastify API exposes 37 route files organized by domain, plus internal service routes.
All routes are prefixed with /api/ and require JWT authentication (except auth,
public forms, unsubscribe, health, and internal service endpoints). Route handlers delegate
to service files for business logic.
| File | Endpoints |
|---|---|
contacts.ts | CRUD for contacts, merge duplicates, activity timeline, custom fields, tags |
companies.ts | CRUD for companies, linked contacts, custom fields, enrichment |
deals.ts | CRUD for deals, stage transitions, pipeline filtering, weighted values |
activities.ts | Log and list activities (emails, calls, meetings, notes) |
tasks.ts | CRUD for tasks, assignment, due date filtering, bulk actions |
pipelines.ts | Pipeline and stage management, ordering, default probabilities |
search.ts | Global search across contacts, companies, deals, tickets |
leadScoring.ts | CRUD for scoring rules (positive, negative, decay) |
| File | Endpoints |
|---|---|
campaigns.ts | Campaign CRUD, send, schedule, A/B testing, statistics |
emailTemplates.ts | Template CRUD, preview, test send, merge variable listing |
sequences.ts | Drip sequence CRUD, step management, enrollment control |
segments.ts | Dynamic/static segment CRUD, criteria definition, member listing |
forms.ts | Form CRUD, field definitions, public submission endpoint |
landingPages.ts | Landing page CRUD, publish/unpublish, conversion tracking |
| File | Endpoints |
|---|---|
products.ts | Product catalog CRUD, pricing, categories |
quotes.ts | Quote CRUD, line items, PDF export, public view token |
tickets.ts | Ticket CRUD, SERIAL number assignment, SLA tracking, assignment |
knowledgeBase.ts | Article CRUD, categories, public/internal visibility |
surveys.ts | Survey CRUD, question management, public response endpoint, results |
pdf.ts | PDF generation for quotes and itineraries via pdf-lib |
| File | Endpoints |
|---|---|
workflows.ts | Workflow CRUD, step definitions, trigger configuration, enrollment management |
emailCompose.ts | Compose and send individual emails with template selection |
tracking.ts | Email open pixel, click redirect, page view tracking |
| File | Endpoints |
|---|---|
auth.ts | Login, register, refresh token, logout, password reset |
dashboard.ts | KPI aggregation for the main dashboard (contacts, deals, revenue, tickets) |
notifications.ts | User notification listing, mark-as-read, Socket.io subscription |
webhooks.ts | Inbound webhook processing from ESP and external services |
slack.ts | Slack event handling with signing secret verification |
slackCommands.ts | Slack slash command handlers |
n8n.ts | n8n webhook endpoints (/api/n8n/webhook/:action) with timing-safe X-Webhook-Secret verification. Three callback actions: passport-alert, commission-reconcile, booking-sync — each validated with a Zod schema. |
uploads.ts | File upload with multipart processing |
meetings.ts | Meeting scheduling, booking pages, calendar integration |
unsubscribe.ts | Email unsubscribe processing (public endpoint) |
portal.ts | Client self-service portal (login, dashboard) |
reports.ts | Pre-built report queries with date range filtering |
settings.ts | System configuration (admin only) |
integrations/ | 14 sub-routes: travelProxy, slackHub, n8nConsole, queueAdmin, ai, analytics, google, health, leadPipeline, social, trips, website, groups |
Machine-to-machine endpoints registered under /api/internal in
server.ts. Protected by serviceAuth.ts middleware
(timing-safe x-internal-service header check) instead of JWT auth.
| Endpoint | Purpose |
|---|---|
POST /api/internal/contacts | Sync contacts from TT-API — creates or updates contacts in Nexus CRM |
POST /api/internal/deals | Sync deals from TT-API — creates or updates deals in Nexus CRM |
React Frontend
The frontend is a React 19 SPA built with Vite, located in nexus/src/client/
with its own package.json. It uses React Router for client-side navigation
and lazy-loads all page components via React.lazy() for code splitting.
State management uses Zustand stores for auth and notifications, with component-local
state for everything else.
Page Groups (80+ components)
| Category | Pages |
|---|---|
| CRM | ContactList, ContactDetail, DuplicateManager, CompanyList, CompanyDetail, DealKanban, DealList, DealDetail, TaskList |
| Marketing | EmailTemplateList, EmailTemplateEditor, CampaignList, CampaignWizard, CampaignDetail, SegmentList, SegmentBuilder, FormList, FormBuilder, LandingPageList, LandingPageBuilder, LeadScoringRules |
| Automation | WorkflowList, WorkflowDetail, SequenceList, SequenceBuilder |
| Sales | ProductList, ProductDetail, QuoteList, QuoteBuilder, QuotePublicView, PlaybookList, PlaybookEditor |
| Service | TicketList, TicketDetail, ArticleList, ArticleEditor, CategoryManager, SurveyList, SurveyBuilder, SurveyResults, SurveyPublic |
| Travel | TripList, TripDetail, BookingList, BookingDetail, TravelerList, TravelerDetail, SupplierList, SupplierDetail, FinanceDashboard, FinanceCommissions, FinanceInvoices, FinancePaymentSchedules, GroupTripDetail, GroupsSocialFeed, TravelAlerts, SatisfactionDashboard, ItineraryBuilder, VendorContracts |
| Clients | ClientList, ClientDetail, ClientForm |
| Ops Hub | SlackHub, SystemHealth, N8nConsole, ContentHub, CampaignTracker, LeadPipeline, GoogleWorkspace, AiAssistant, GroupsDashboard, TripsHub, AnalyticsDashboard, WebsiteManager, AutomationMonitor |
| Content | AssetLibrary, ContentManager, ContentEditor, SiteAnalytics |
| System | Dashboard, Login, ReportsPage, SettingsPage, NotFound |
| Public | MeetingBooking, QuotePublicView, SurveyPublic, PortalLogin, PortalDashboard |
Frontend Architecture
- AppLayout — Wraps all authenticated routes with sidebar navigation, header, and notification panel
- ProtectedRoute — Guards authenticated routes, redirects to
/loginif no valid session - Zustand Stores —
authStore(JWT, user info, refresh logic) andnotificationStore(Socket.io events, unread count) - API Client — Centralized fetch wrapper in
lib/with automatic token attachment and refresh - TanStack Table — Used for all data table views with server-side sorting, filtering, and pagination
- @dnd-kit — Drag-and-drop for deal Kanban boards, workflow step ordering, and pipeline stage management
Build output:
vite buildproduces a static bundle innexus/src/client/dist/that thenexus-webcontainer serves via nginx. The nginx config includes CSP headers and a catch-all rule that servesindex.htmlfor client-side routing.
BullMQ Workers
Five dedicated worker processors run in the nexus-worker container, consuming
jobs from Redis-backed BullMQ queues. Each worker has concurrency 5 and provides automatic
retries with exponential backoff. A sixth process — the scheduled campaign checker —
runs on a 60-second interval to trigger campaigns whose sendAt time has arrived.
Worker Resilience (Round 11)
All four worker processors now include hardened error handling:
'error'event handler on every worker — catches connection errors and unexpected failures without crashing the process'stalled'event handler on every worker — logs stalled job IDs for investigationmaxStalledCount: 2on all workers — jobs that stall twice are moved to the failed set- Graceful shutdown with a 10-second timeout using a
Promise.racepattern — ensures workers drain cleanly on SIGTERM without hanging indefinitely
Sends individual emails via the configured ESP. Each job represents one recipient for a campaign, sequence step, or ad-hoc email. Handles merge variable interpolation at send time.
- Tracks campaign completion: atomically increments
totalSent, flips status tosentwhen all recipients processed - Supports A/B test variants via
variantIdfield - Failed sends retry with exponential backoff (3 attempts)
- Dead-lettered messages trigger alert notifications
Processes inbound webhook events from the ESP — opens, clicks, bounces, spam complaints, and unsubscribes. Updates campaign statistics and contact engagement records.
- Deduplicates events by message ID + event type
- Bounces and complaints trigger automatic contact status updates
- Click events resolve redirect URLs and record the target
- Top-level try/catch with context logging (job ID, event type) for debugging failures
Executes workflow steps asynchronously. Each completed step schedules the next, forming an execution chain. Also handles lead score recalculation via the lead-score-recalculate job name.
- Step types:
send_email,update_property,add_to_segment,wait,branch,call_webhook - Branch steps evaluate conditions and route to different paths
- Wait steps schedule a delayed job (minutes to days)
- Uses
WorkflowEnrollmentRowtype alias — 10anycasts removed for proper typing
Handles time-based execution for email sequences. Processes sequence-step jobs that fire at configured delay intervals between sequence steps.
- Checks unenroll conditions before each step (replied, converted, manually removed)
- Respects send windows (business hours, timezone-aware)
- Coordinates with the Email Worker for actual delivery
- Uses
SequenceEnrollmentRowtype alias with typedcntfield for step counting
Refreshes dynamic segment membership. Evaluates segment criteria against all contacts and replaces the current member set with the freshly computed result.
- Atomic replace: deletes all current members, inserts the evaluated set
- Logs member count after each refresh
- Runs on demand when segments are modified or on a configurable schedule
Scheduled Campaign Checker: In addition to the 5 queue workers, an
setIntervalloop runs every 60 seconds to find campaigns withstatus='scheduled'andsendAt ≤ now, then triggers their send. This runs in the same worker process.
Travel Proxy
Nexus proxies requests to the main TT-API for travel data that lives in
tt_fresh_db. The proxy layer at /api/travel/* forwards requests
over the Docker internal network to http://tt-api:3100, adding resilience
patterns to protect against downstream failures. The TT-API validates requests using
the TT_API_KEY header.
Resilience Patterns
| Pattern | Configuration | Env Override |
|---|---|---|
| Timeout | 10 seconds per request | TT_PROXY_TIMEOUT_MS |
| Retry with Backoff | 1 retry, 500 ms base delay (exponential) | TT_PROXY_RETRY_COUNT, TT_PROXY_RETRY_BASE_MS |
| Circuit Breaker | Opens after 5 failures within 30 s window; tests recovery after 30 s; closes after 2 consecutive successes | TT_CB_FAILURE_THRESHOLD, TT_CB_RESET_TIMEOUT_MS, TT_CB_SUCCESS_THRESHOLD |
Circuit Breaker States
Closed (Normal)
All requests pass through. Failures within the window are counted. If failures exceed the threshold, circuit opens.
Open (Failing Fast)
All requests immediately fail with a circuit-open error. After the reset timeout, transitions to half-open.
Half-Open (Testing)
Allows requests through. If enough consecutive successes occur, closes the circuit. Any failure reopens it.
Internal network only. The travel proxy communicates over Docker's internal bridge network. It is not exposed to the public internet. Travel data (clients, bookings, trips) in TT-API is the source of truth — Nexus travel routes are deprecated in favor of the proxy.
Slack Integration
Nexus integrates with Slack at two levels: inbound event processing
(Slack sends events to Nexus) and outbound messaging (Nexus posts to
Slack channels). The signing secret is required — if
SLACK_SIGNING_SECRET is not set, the endpoint returns 503.
Inbound: Event Handling
- Webhook verification — All inbound requests are verified using the Slack signing secret with HMAC-SHA256. Invalid signatures are rejected with 401.
- Event processing — Supported events include messages, reactions, app mentions, and interactive actions.
- Slash commands — Custom slash commands for creating tickets, looking up contacts, and checking deal status directly from Slack.
- Request tracking — Slack requests are tracked with type (
nx_slack_request_type) and status (nx_slack_request_status) enums.
Outbound: Notifications
- Ticket assignments and SLA breaches posted to support channels
- Deal stage changes posted to sales channels
- Campaign completion summaries posted to marketing channels
- System health alerts posted to the #monitoring channel
Route Files
| File | Purpose |
|---|---|
routes/slack.ts | Slack event webhooks with signing secret verification |
routes/slackCommands.ts | Slash command handlers |
routes/integrations/slackHub.ts | Slack Hub dashboard data (channel overview, message counts) |
Email Branding & Compliance
The unsubscribeService.ts provides a getComplianceFooter() function
that generates CAN-SPAM compliant email footers for all outbound marketing emails. The footer
is styled with the Dark Sanctuary brand identity for a cohesive experience.
Footer Styling
| Property | Value |
|---|---|
| Background | Navy #0B1120 |
| Accent color | Gold #C9A84C |
| Font family | Georgia serif (email-safe Playfair Display fallback) |
| Secondary text | Off-white at 60% opacity |
| Compliance | CAN-SPAM compliant with one-click unsubscribe link |
Unsubscribe links are generated with HMAC using the
UNSUBSCRIBE_SECRETenv var. The unsubscribe endpoint is public (no JWT required) and processes opt-outs immediately. Production startup warns if the default placeholder secret is still in use.
Development
Local development requires two terminal sessions: one for the Fastify backend (tsx watch)
and one for the Vite React frontend. The frontend has its own package.json
inside nexus/src/client/.
Initial Setup
cd nexus/
npm install # Backend dependencies
cd src/client && npm install # Frontend dependencies
cd ../.. # Back to nexus root
Backend (Terminal 1)
npm run dev # tsx watch src/index.ts (hot-reload)
npm run typecheck # tsc --noEmit
npm run lint # eslint src/
npm test # vitest run
npm run test:watch # vitest (watch mode)
Frontend (Terminal 2)
cd src/client
npx vite dev # Vite dev server with HMR
Database
npm run migrate # tsx src/db/migrate.ts
Full Build
npm run build # tsc (backend) + vite build (frontend)
Environment variables: Copy
.env.exampleto.envand fill inDATABASE_URL,REDIS_URL,JWT_SECRET, andJWT_REFRESH_SECRETat minimum. All external service integrations (Slack, ESP, Claude, Google, Sentry) are optional in development and log to console when keys are absent.
Project Structure
nexus/
src/
index.ts # Fastify entry point (Sentry init, graceful shutdown)
server.ts # App builder (plugin + route registration)
config/ # Env config and validation
db/ # Drizzle schema (16 modules), migrations, seed, connection pool
routes/ # 37 Fastify route files
routes/integrations/ # 14 integration sub-routes (travel proxy, Slack, n8n, etc.)
services/ # Business logic (56+ service files)
middleware/ # authenticate.ts, serviceAuth.ts, rbac.ts, error handling, file upload
plugins/ # Fastify plugins (CORS, rate limit, Socket.io, Swagger)
workers/ # BullMQ job processors (4 files + index.ts)
queues/ # BullMQ queue definitions
templates/ # Email templates (React Email)
lib/ # Redis client, utilities
client/ # React SPA (separate package.json)
src/
pages/ # 80+ route page components
components/ # Shared + feature components, AppLayout
stores/ # Zustand stores (auth, notifications)
lib/ # API client, utilities
router.tsx # React Router configuration
docker/
Dockerfile.api # Multi-stage Node.js 20 build
Dockerfile.web # Vite build -> nginx:alpine
docker-compose.yml # 5-container orchestration
nginx-frontend.conf # CSP headers, SPA routing fallback
Deployment
Nexus has no git remote. Deployment is done via tar archive over SSH,
followed by a Docker Compose rebuild on the VPS. The source lives at
/opt/nexus-crm on the production server.
Deploy Process
Clean Remote
Remove stale source files on VPS to prevent old files from persisting: ssh shane@VPS "sudo rm -rf /opt/nexus-crm/src/"
Tar Deploy
From the nexus/ directory: tar --exclude=node_modules --exclude=.git -cf - . | ssh shane@VPS "sudo tar xf - -C /opt/nexus-crm/"
Rebuild Containers
On the VPS: cd /opt/nexus-crm/docker && sudo docker compose up -d --build
Critical: The
docker/.envfile must containPOSTGRES_PASSWORDfor Docker Compose variable interpolation. This is separate from the main.envfile. Without it, the database connection URL will have an empty password and containers will fail to start.
Traefik Routing
| Path Pattern | Target |
|---|---|
/api/*, /socket.io/*, /lp/*, /kb/*, /forms/*, /unsubscribe/*, /meet/* | nexus-api:3000 |
Everything else (/*) | nexus-web:80 (priority 1, catch-all) |
Environment Variables (Production)
Required
DATABASE_URLREDIS_URLJWT_SECRETJWT_REFRESH_SECRETPOSTGRES_PASSWORD(docker/.env)
Security-Sensitive
UNSUBSCRIBE_SECRET— Used for email unsubscribe link HMAC. Production startup now warns if the default placeholder value is still in use. Must be changed before go-live.N8N_WEBHOOK_SECRET— Shared secret for n8n callback authentication. Added to.env.exampleanddocker-compose.yml. The/api/n8n/webhook/:actionroute verifies this via timing-safe comparison of theX-Webhook-Secretheader.TT_INTERNAL_SERVICE_SECRET— Shared secret for internal service auth. Used byserviceAuth.tsmiddleware on/api/internal/*routes. Must match the value in TT-API's.env. Returns 503 if not set.
Optional (Graceful Degradation)
SLACK_SIGNING_SECRET,SLACK_BOT_TOKENSENTRY_DSNTT_API_URL,TT_API_KEYN8N_URL,N8N_API_KEYGOOGLE_SERVICE_ACCOUNT_KEY_FILE
Screenshots
Visual reference for the Nexus Ops Hub interface.