This document describes the architecture of the Trigger.dev web application, which serves both the dashboard UI and API endpoints. The webapp is a Remix 2.1.0 application running on an Express server, built as a containerized service that can run standalone or in a clustered configuration.
For information about the navigation system and routing patterns, see Navigation and Routing. For details on real-time updates and SSE, see Realtime Updates and SSE. For Socket.IO and WebSocket communication, see Socket.IO and WebSocket Communication.
The web application is built on the following core technologies:
| Component | Technology | Version | Purpose |
|---|---|---|---|
| Framework | Remix | 2.1.0 | Server-side rendering, routing, data loading |
| HTTP Server | Express | 4.20.0 | HTTP server, middleware pipeline |
| Runtime | Node.js | ≥18.19.0 or ≥20.6.0 | JavaScript runtime |
| UI Library | React | 18.2.0 | Component rendering |
| Database ORM | Prisma | 6.14.0 | Database access via @trigger.dev/database |
| Build Tool | esbuild | 0.15.10+ | TypeScript compilation, bundling |
| Monorepo | pnpm + Turborepo | 8.15.5 + 1.10.3 | Package management, build orchestration |
| Container | Docker | Multi-stage | Production deployment |
Sources: apps/webapp/package.json1-289 package.json1-95
The web application combines Express as the HTTP server with Remix as the application framework. This integration is managed in apps/webapp/server.ts1-269 which:
Sources: apps/webapp/server.ts91-269
The server initialization follows this sequence:
Sources: apps/webapp/server.ts1-269
Requests flow through the following middleware in order:
Middleware Configuration:
| Middleware | Purpose | Configurable | File Reference |
|---|---|---|---|
compression() | Gzip compression | DISABLE_COMPRESSION | server.ts93-95 |
| Static assets | Serve /build and /public | Cache headers | server.ts101-105 |
morgan() | Request logging | Always enabled | server.ts107 |
| Security headers | XSS, robots, HSTS | Based on hostname | server.ts126-143 |
| Request ID | Unique ID per request | Always enabled | server.ts145-153 |
| Route filtering | Restrict to /realtime | ALLOW_ONLY_REALTIME_API | server.ts156-167 |
| API rate limiter | Public API throttling | See API Authentication | server.ts169 |
| Engine rate limiter | Internal API throttling | Configuration in env | server.ts170 |
| Remix handler | Application logic | N/A | server.ts172-179 |
Sources: apps/webapp/server.ts93-186
Each request receives a unique request ID and HTTP context stored in async local storage:
This context is accessible throughout the request lifecycle via httpAsyncStorage. The context includes:
requestId: Unique identifier (generated by nanoid())path: Request URL pathhost: Request hostnamemethod: HTTP methodSources: apps/webapp/server.ts145-153 apps/webapp/app/services/httpAsyncStorage.server.ts (referenced)
The webapp supports optional clustering to utilize multiple CPU cores. When enabled, it runs in a primary-worker model.
| Environment Variable | Default | Description |
|---|---|---|
ENABLE_CLUSTER | "0" | Enable clustering mode |
CLUSTER_WORKERS | CPU count | Number of worker processes |
WEB_CONCURRENCY | CPU count | Alternative worker count variable |
GRACEFUL_SHUTDOWN_TIMEOUT | 30000 ms | Max time to wait for graceful shutdown |
Cluster Behavior:
Worker Count Determination:
CLUSTER_WORKERS or WEB_CONCURRENCY if setos.availableParallelism() (CPU count)Primary Process Responsibilities:
"node webapp-server primary" (server.ts74)Worker Process Responsibilities:
"node webapp-worker-{id}" (server.ts109-111)Graceful Shutdown:
SIGTERM/SIGINT to all workersSources: apps/webapp/server.ts15-90
The production build uses a multi-stage Dockerfile optimized for layer caching and minimal image size.
Build Stage Purposes:
| Stage | Base Image | Purpose | Key Files |
|---|---|---|---|
goose_builder | golang:1.23-alpine | Build Goose migration tool | ClickHouse migrations |
pruner | Node | Use Turborepo to prune workspace | turbo prune --scope=webapp |
base | Node | Set up base dependencies | .gitignore, package.json, lock file |
dev-deps | base | Install dev dependencies | For building TypeScript |
production-deps | base | Install production deps only | For runtime |
builder | base + dev-deps | Build webapp, generate Prisma | Compiled assets |
runner | base + production-deps | Final runtime image | Built artifacts only |
Build Commands:
Sources: docker/Dockerfile1-116 apps/webapp/package.json7-11
The monorepo uses Turborepo to orchestrate builds with dependency tracking:
Sources: turbo.json1-147 apps/webapp/package.json7-11
All environment variables are validated and typed using Zod schemas in apps/webapp/app/env.server.ts1-685 This provides:
Configuration Pattern:
| Category | Example Variables | Purpose |
|---|---|---|
| Core Application | NODE_ENV, APP_ORIGIN, LOGIN_ORIGIN | Basic app configuration |
| Database | DATABASE_URL, DIRECT_URL, DATABASE_READ_REPLICA_URL | PostgreSQL connections |
| Redis (Segmented) | REDIS_HOST, CACHE_REDIS_HOST, PUBSUB_REDIS_HOST | Different Redis instances |
| Authentication | SESSION_SECRET, MAGIC_LINK_SECRET, ENCRYPTION_KEY | Auth and encryption |
| External Services | RESEND_API_KEY, OPENAI_API_KEY, DEPOT_TOKEN | Third-party integrations |
| Deployment | DEPLOY_REGISTRY_HOST, DEPOT_ORG_ID | Docker registry config |
| Observability | INTERNAL_OTEL_TRACE_EXPORTER_URL, POSTHOG_PROJECT_KEY | Telemetry and analytics |
| Run Engine | RUN_ENGINE_WORKER_COUNT, RUN_ENGINE_TIMEOUT_EXECUTING | Task execution tuning |
| Rate Limiting | API_RATE_LIMIT_MAX, API_RATE_LIMIT_REFILL_RATE | API throttling |
| Feature Flags | MARQS_WORKER_ENABLED, V2_MARQS_ENABLED | Feature toggles |
Redis Segmentation:
The system uses multiple logical Redis instances for different purposes:
REDIS_HOST, REDIS_PORT - Default instanceRATE_LIMIT_REDIS_HOST - Token bucket rate limitingCACHE_REDIS_HOST - Application cachingREALTIME_STREAMS_REDIS_HOST - SSE event streamsPUBSUB_REDIS_HOST - Event bus communicationRUN_ENGINE_WORKER_REDIS_HOST - Run engine coordinationMARQS_REDIS_HOST - Message queue systemEach Redis instance supports separate reader/writer hosts for read replica configurations.
Sources: apps/webapp/app/env.server.ts1-685
Security headers are set on all responses:
Additional headers from root.tsx:
Sources: apps/webapp/server.ts126-143 apps/webapp/app/root.tsx23-28
Gzip compression is enabled by default for all responses (unless DISABLE_COMPRESSION=1):
Sources: apps/webapp/server.ts93-95
Trailing slashes are removed via 301 redirects:
Sources: apps/webapp/server.ts136-142
The server handles WebSocket upgrades for both Socket.IO and plain WebSocket connections:
Upgrade Logic:
Sources: apps/webapp/server.ts225-263
Socket.IO is attached to the HTTP server and configured with Redis adapter for multi-instance support:
socketIo?.io.attach(server) (server.ts225)Sources: apps/webapp/server.ts120-226 apps/webapp/app/v3/handleSocketIo.server.ts (referenced)
The webapp uses different caching strategies for different asset types:
| Path | Cache Duration | Immutable | Purpose |
|---|---|---|---|
/build/* | 1 year | Yes | Remix fingerprinted assets (CSS, JS) |
/public/* | 1 hour | No | Static files (favicon, images) |
Configuration:
Remix automatically fingerprints assets during build, allowing aggressive caching with immutable flag. This means:
entry.client-ABC123.jsSources: apps/webapp/server.ts101-105
apps/webapp/
├── build/
│ ├── server.js # Compiled server entry point
│ ├── server.js.map # Source map
│ └── index.js # Remix build output
├── public/
│ ├── build/ # Fingerprinted assets
│ │ ├── entry.client-*.js
│ │ ├── root-*.css
│ │ └── routes/ # Route-specific bundles
│ └── [static files] # favicon, images, etc.
└── prisma/
└── seed.js # Database seeding script
Sources: docker/Dockerfile85-88 turbo.json8-14
The server bootstraps in this order:
| Entry Point | Purpose | Execution Context |
|---|---|---|
server.ts | HTTP server setup, Express config | Node.js main process |
entry.server.tsx | Server-side rendering | Per-request (server) |
entry.client.tsx | Client-side hydration | Browser |
root.tsx | Root Remix route, HTML shell | Both server and client |
sentry.server.ts | Error tracking initialization | Server startup |
Server-Side Rendering Flow:
Client-Side Hydration:
Sources: apps/webapp/server.ts1-269 apps/webapp/app/entry.server.tsx1-258 apps/webapp/app/entry.client.tsx1-16 apps/webapp/app/root.tsx1-133
The webapp performs initialization tasks in apps/webapp/app/entry.server.tsx190-257:
Background Services:
Feature Flags on Startup:
Sources: apps/webapp/app/entry.server.tsx190-257
The server handles graceful shutdown on SIGTERM and SIGINT:
In clustered mode, the primary process:
Sources: apps/webapp/server.ts207-223 apps/webapp/server.ts29-71
Errors during request handling are caught by:
handleError exportSources: apps/webapp/app/entry.server.tsx174-228
The root ErrorBoundary component provides a fallback UI for React errors:
Sources: apps/webapp/app/root.tsx83-106
The Docker container uses a multi-step entrypoint script:
Entrypoint Script Steps:
Sources: docker/scripts/entrypoint.sh1-51
The Docker build captures metadata via build arguments:
| Build Arg | Purpose | Used At Runtime |
|---|---|---|
BUILD_APP_VERSION | Application version | Yes (env var) |
BUILD_GIT_SHA | Git commit hash | Yes (env var) |
BUILD_GIT_REF_NAME | Git branch/tag | Yes (env var) |
BUILD_TIMESTAMP_SECONDS | Build timestamp | Yes (env var) |
SENTRY_RELEASE | Sentry release ID | Build time only |
SENTRY_ORG | Sentry organization | Build time only |
SENTRY_PROJECT | Sentry project | Build time only |
These values are used for:
Sources: docker/Dockerfile51-103
| File | Purpose | Format |
|---|---|---|
turbo.json | Turborepo build pipeline | JSON |
package.json | Workspace and script definitions | JSON |
apps/webapp/package.json | Webapp dependencies and scripts | JSON |
remix.config.js | Remix framework configuration | JavaScript |
tailwind.config.ts | Tailwind CSS design system | TypeScript |
tsconfig.json | TypeScript compiler options | JSON |
.dockerignore | Docker build exclusions | Text |
pnpm-workspace.yaml | pnpm workspace packages | YAML |
Sources: turbo.json1-147 package.json1-95 apps/webapp/package.json1-289 .dockerignore1-50
The webapp includes query performance monitoring:
This monitors both writer and replica connections and logs queries exceeding the threshold defined by VERY_SLOW_QUERY_THRESHOLD_MS.
Sources: apps/webapp/app/utils/queryPerformanceMonitor.server.ts1-65
Performance-related configuration options:
| Setting | Environment Variable | Default | Purpose |
|---|---|---|---|
| Keep-alive timeout | (hardcoded) | 65s | Prevent premature connection closure |
| Max headers | server.maxHeadersCount | 0 (unlimited) | Uses maxHeaderSize instead |
| Max old space | NODE_MAX_OLD_SPACE_SIZE | 8192 MB | Node.js heap size limit |
| Worker count | CLUSTER_WORKERS | CPU count | Number of cluster workers |
| Compression | DISABLE_COMPRESSION | Enabled | Gzip response compression |
Sources: apps/webapp/server.ts201-205 docker/scripts/entrypoint.sh44-50
Summary:
The Trigger.dev web application is a production-grade Remix application running on Express, designed for containerized deployment with optional clustering support. It provides both the dashboard UI and API endpoints, with comprehensive middleware, real-time communication capabilities, validated environment configuration, and graceful shutdown handling. The architecture supports horizontal scaling through clustering and integrates deeply with the run engine, background job processing, and event streaming systems.
Refresh this wiki