# Webapp Remix 2.17.4 app serving as the main API, dashboard, and orchestration engine. Uses an Express server (`server.ts`). ## Verifying Changes **Never run `pnpm run build --filter webapp` to verify changes.** Building proves almost nothing about correctness. The webapp is an app, not a public package — use typecheck from the repo root: ```bash pnpm run typecheck --filter webapp # ~1-2 minutes ``` Only run typecheck after major changes (new files, significant refactors, schema changes). For small edits, trust the types and let CI catch issues. Note: Public packages (`packages/*`) use `build` instead. See the root CLAUDE.md for details. ## Testing Dashboard Changes with Chrome DevTools MCP Use the `chrome-devtools` MCP server to visually verify local dashboard changes. The webapp must be running (`pnpm run dev --filter webapp` from repo root). ### Login ``` 1. mcp__chrome-devtools__new_page(url: "http://localhost:3030") → Redirects to /login 2. mcp__chrome-devtools__click the "Continue with Email" link 3. mcp__chrome-devtools__fill the email field with "local@trigger.dev" 4. mcp__chrome-devtools__click "Send a magic link" → Auto-logs in and redirects to the dashboard (no email verification needed locally) ``` ### Navigating and Verifying - **take_snapshot**: Get an a11y tree of the page (text content, element UIDs for interaction). Prefer this over screenshots for understanding page structure. - **take_screenshot**: Capture what the page looks like visually. Use to verify styling, layout, and visual changes. - **navigate_page**: Go to specific URLs, e.g. `http://localhost:3030/orgs/references-bc08/projects/hello-world-SiWs/env/dev/runs` - **click / fill**: Interact with elements using UIDs from `take_snapshot`. - **evaluate_script**: Run JS in the browser console for debugging. - **list_console_messages**: Check for console errors after navigating. ### Tips - Snapshots can be very large on complex pages (200K+ chars). Use `take_screenshot` first to orient, then `take_snapshot` only when you need element UIDs to interact. - The local seeded user email is `local@trigger.dev`. - Dashboard URL pattern: `http://localhost:3030/orgs/{orgSlug}/projects/{projectSlug}/env/{envSlug}/{section}` ## Key File Locations - **Trigger API**: `app/routes/api.v1.tasks.$taskId.trigger.ts` - **Batch trigger**: `app/routes/api.v1.tasks.batch.ts` - **OTEL endpoints**: `app/routes/otel.v1.logs.ts`, `app/routes/otel.v1.traces.ts` - **Prisma setup**: `app/db.server.ts` - **Run engine config**: `app/v3/runEngine.server.ts` - **Services**: `app/v3/services/**/*.server.ts` - **Presenters**: `app/v3/presenters/**/*.server.ts` ## Route Convention Routes use Remix flat-file convention with dot-separated segments: `api.v1.tasks.$taskId.trigger.ts` -> `/api/v1/tasks/:taskId/trigger` ## Abort Signals **Never use `request.signal`** for detecting client disconnects. It is broken due to a Node.js bug ([nodejs/node#55428](https://github.com/nodejs/node/issues/55428)) where the AbortSignal chain is severed when Remix internally clones the Request object. Instead, use `getRequestAbortSignal()` from `app/services/httpAsyncStorage.server.ts`, which is wired directly to Express `res.on("close")` and fires reliably. ```typescript import { getRequestAbortSignal } from "~/services/httpAsyncStorage.server"; // In route handlers, SSE streams, or any server-side code: const signal = getRequestAbortSignal(); ``` ## Environment Variables Access via `env` export from `app/env.server.ts`. **Never use `process.env` directly.** For testable code, **never import env.server.ts** in test files. Pass configuration as options instead: - `realtime/nativeRealtimeClient.server.ts` (testable service, takes config as constructor arg) - `realtime/nativeRealtimeClientInstance.server.ts` (creates singleton with env config) ## Run Engine 2.0 The webapp integrates `@internal/run-engine` via `app/v3/runEngine.server.ts`. This is the singleton engine instance. Services in `app/v3/services/` call engine methods for all run lifecycle operations (triggering, completing, cancelling, etc.). The `engineVersion.server.ts` file determines V1 vs V2 for a given environment. New code should always target V2. ## Background Workers Background job workers use `@trigger.dev/redis-worker`: - `app/v3/commonWorker.server.ts` - `app/v3/alertsWorker.server.ts` - `app/v3/batchTriggerWorker.server.ts` Do NOT add new jobs using zodworker/graphile-worker (legacy). ## Real-time - Socket.io: `app/v3/handleSocketIo.server.ts`, `app/v3/handleWebsockets.server.ts` - Electric SQL: Powers real-time data sync for the dashboard ## Legacy V1 Code The `app/v3/` directory name is misleading - most code is actively used by V2. Only these specific files are V1-only legacy: - `app/v3/marqs/` (old MarQS queue system) - `app/v3/legacyRunEngineWorker.server.ts` - `app/v3/services/triggerTaskV1.server.ts` - `app/v3/services/cancelTaskRunV1.server.ts` - `app/v3/authenticatedSocketConnection.server.ts` - `app/v3/sharedSocketConnection.ts` Some services (e.g., `cancelTaskRun.server.ts`, `batchTriggerV3.server.ts`) branch on `RunEngineVersion` to support both V1 and V2. When editing these, only modify V2 code paths. ## Performance: Trigger Hot Path The `triggerTask.server.ts` service is the **highest-throughput code path** in the system. Every API trigger call goes through it. Keep it fast: - **Do NOT add database queries** to `triggerTask.server.ts` or `batchTriggerV3.server.ts`. Task defaults (TTL, etc.) are resolved via `backgroundWorkerTask.findFirst()` in the queue concern (`queues.server.ts`) - one query per request, in mutually exclusive branches depending on locked/non-locked path. Piggyback on the existing query instead of adding new ones. - **Two-stage resolution pattern**: Task metadata is resolved in two stages by design: 1. **Trigger time** (`triggerTask.server.ts`): Only TTL is resolved from task defaults. Everything else uses whatever the caller provides. 2. **Dequeue time** (`dequeueSystem.ts`): Full `BackgroundWorkerTask` is loaded and retry config, machine config, maxDuration, etc. are resolved against task defaults. - If you need to add a new task-level default, **add it to the existing `select` clause** in the `backgroundWorkerTask.findFirst()` query — do NOT add a second query. If the default doesn't need to be known at trigger time, resolve it at dequeue time instead. - Batch triggers (`batchTriggerV3.server.ts`) follow the same pattern — keep batch paths equally fast. ## Prisma Query Patterns - **Always use `findFirst` instead of `findUnique`.** Prisma's `findUnique` has an implicit DataLoader that batches concurrent calls into a single `IN` query. This batching cannot be disabled and has active bugs even in Prisma 6.x: uppercase UUIDs returning null (#25484, confirmed 6.4.1), composite key SQL correctness issues (#22202), and 5-10x worse performance than manual DataLoader (#6573, open since 2021). `findFirst` is never batched and avoids this entire class of issues. ## React Patterns - Only use `useCallback`/`useMemo` for context provider values, expensive derived data that is a dependency elsewhere, or stable refs required by a dependency array. Don't wrap ordinary event handlers or trivial computations. - Use named constants for sentinel/placeholder values (e.g. `const UNSET_VALUE = "__unset__"`) instead of raw string literals scattered across comparisons.