## What Wires the run-ops split into the webapp: database topology, environment flags, split-mode gating, and the control-plane resolver/cache layer that the run-store and run-engine seams from the previous PR plug into. - **DB topology & env** (`apps/webapp/app/db.server.ts`, `env.server.ts`, `entry.server.tsx`): adds the run-ops database clients/topology and the environment variables that configure and gate the split. - **runOpsMigration module** (new `apps/webapp/app/v3/runOpsMigration/`): the webapp-side machinery — `splitMode.server.ts`, `controlPlaneResolver.server.ts` + `controlPlaneCache.server.ts`, `readThrough.server.ts`, `crossSeamGuard.server.ts`, `distinctDbSentinel.server.ts`, id-minting helpers (`mintBatchFriendlyId`, `runOpsMintKind`, `resolveInheritedMintKind`), `runOpsCascadeCleanup.server.ts`, the split read gate, and route/unblock catalogs. - **Store/engine wiring** (`app/v3/runStore.server.ts`, `runEngine.server.ts`, `runEngineHandlers.server.ts` + new `runEngineHandlersShared.server.ts`): points the webapp's store/engine construction at the resolver, and factors shared handler logic out so both seams use one path. - **Read-path touch-ups**: `runtimeEnvironment.server.ts`, `eventRepository/index.server.ts`, `taskRunHeartbeatFailed.server.ts`, `engineVersion.server.ts` route their run/environment lookups read-through the resolver. - `413a94511` — interlocks split mode against the native realtime backend so the two aren't enabled in an incompatible combination (see `.server-changes/run-ops-split-realtime-interlock.md`). - `dc74c57fd` — drops the earlier "known-migrated" read layer; residency is determined by id-shape only. ## Why PR5 of the run-ops split stack. This is the webapp foundation layer: it stands up the DB topology, flags, and resolver/cache the rest of the stack depends on, and repoints webapp read paths through the resolver. Additive when the split is not enabled (existing single-DB behavior preserved behind flags); behavior-changing on the read-through paths and the realtime interlock. ## Tests New vitest coverage across `apps/webapp/test/` and colocated `*.server.test.ts` files: db topology, split mode, split read gate, cross-seam guard, mint cutover / flip latency, control-plane cache, control-plane resolver, distinct-db sentinel, read-through loaders (route loaders, run-detail loaders, `findEnvironmentFromRun`), and the run-engine handlers. Testcontainers-backed; no mocks. `pnpm-lock.yaml` synced for the two new webapp deps. ## Notes Draft, **stacked on #4116** (`runops/pr04-store-engine`). Review that first; this diff is against it. Server-change / changeset note to be added at stack-assembly time. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.3 KiB
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:
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_screenshotfirst to orient, thentake_snapshotonly 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) 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.
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.tsapp/v3/alertsWorker.server.tsapp/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.tsapp/v3/services/triggerTaskV1.server.tsapp/v3/services/cancelTaskRunV1.server.tsapp/v3/authenticatedSocketConnection.server.tsapp/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.tsorbatchTriggerV3.server.ts. Task defaults (TTL, etc.) are resolved viabackgroundWorkerTask.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:
- Trigger time (
triggerTask.server.ts): Only TTL is resolved from task defaults. Everything else uses whatever the caller provides. - Dequeue time (
dequeueSystem.ts): FullBackgroundWorkerTaskis loaded and retry config, machine config, maxDuration, etc. are resolved against task defaults.
- Trigger time (
- If you need to add a new task-level default, add it to the existing
selectclause in thebackgroundWorkerTask.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
findFirstinstead offindUnique. Prisma'sfindUniquehas an implicit DataLoader that batches concurrent calls into a singleINquery. 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).findFirstis never batched and avoids this entire class of issues.
React Patterns
- Only use
useCallback/useMemofor 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.