Skip to content

Worker boundaries

The Cloudflare migration splits the runtime into three Workers:

  • web-tanstack is the app/domain Worker. It owns user-facing product domains such as workspaces, documents, resource pins, profile, feeds, collections, and Stripe-backed subscription state.
  • workers/chat-worker is the chat/agent Worker. It owns chat sessions, AI execution, tool orchestration, podcast artifacts, and AI billing enforcement.
  • workers/core-worker is the engine Worker. It owns scraping, ingestion, AI Search synchronization, entity extraction, queues, and Workflows.
Worker Public surface Service bindings Owns writes for Does not own
web-tanstack TanStack Start app, auth routes, app API routes, /api/media/asset/* CORE_WORKER for engine RPC, CHAT_WORKER for authenticated Agent proxy traffic Workspaces, documents, resource pins, collections, feeds, profile, subscriptions, app-side settings, user_files blob create/quota/R2 put/serve/delete Chat session/message state, AI execution, scraping/search engine internals
workers/chat-worker Service-bound /agents/chat-agent/:sessionId WebSocket/HTTP routes and daily idle-session GC CORE for corpus RPC, DOMAIN for app-domain RPC Durable chat state, tool orchestration, AI cost policy, podcast rows/audio workflow Workspace/document/resource SQL, corpus implementation, subscription and credit-ledger SQL
workers/core-worker /health, cron handlers, Workflows, and service RPC DOMAIN only for app-owned resource image rehosting Scraping, ingestion, resource enrichment, AI Search synchronization, entities, core workflows, source/temp R2 writes Workspace/document/resource product writes, chat state, subscription policy, app-owned blob lifecycle
  • Core service DTOs live with the engine methods that own them, primarily workers/core-worker/src/corpus.ts and workers/core-worker/src/okf.ts.
  • DomainRpc DTOs (web-tanstack/src/server/domain/contracts.ts) are app-domain-owned: workspace/document/resource writes and reads needed by chat tools.
  • Browser traffic reaches the app Worker over normal HTTP/WebSocket. The app authenticates chat requests and proxies them to CHAT_WORKER with the user identity header.
  • The app Worker calls Core through CORE_WORKER; Chat calls Core through CORE; Chat and Core call the app-owned DomainRpc through DOMAIN.
  • Core exposes only /health over fetch. Ingest, search, export, and workflow operations are service RPC, scheduled handlers, or Workflow entrypoints rather than internal-token HTTP endpoints.

There is no generic shared contract package. Cloudflare’s preferred model is that the target WorkerEntrypoint class is the runtime service contract, and callers can generate typed bindings from the caller and callee Wrangler configs when that stays local to the service boundary:

Terminal window
pnpm exec wrangler types -c wrangler.jsonc -c ../workers/core-worker/wrangler.jsonc

The chat worker does the same for the core binding:

Terminal window
pnpm exec wrangler types -c wrangler.jsonc -c ../core-worker/wrangler.jsonc

That makes service bindings carry their WorkerEntrypoint method signatures directly:

  • CORE_WORKER: Service<typeof import("../workers/core-worker/src/index").default>
  • CORE: Service<typeof import("../core-worker/src/index").default>

Do not add new shared service API packages. Service bindings are the runtime boundary; service DTOs live with the Worker that owns the service. Callers can import that owner-published type directly when TypeScript needs help, but product behavior still stays behind the service binding.

Current implementation state: app/chat CORE runtime calls go through Cloudflare service bindings typed by multi-config wrangler types; there is no @core-rpc/contracts package or handwritten CoreRpc interface. DomainRpc runtime calls also go through the app Worker service binding; chat still imports app-owned public DTOs from web-tanstack where tool inputs need named product-domain shapes. The chat worker does not multi-config typegen the whole TanStack app Worker because that would pull app implementation modules into the chat TypeScript program. The chat worker does not keep a src/shared barrel; its auth, DB, env, and logging helpers live under workers/chat-worker/src/chat.

Do not make Workers share ambiguous internal path aliases. The core worker uses owner-scoped @core-shared/* for its private helpers so a caller compiling generated service binding imports cannot accidentally resolve core internals to chat-owned modules.

Multi-config typegen is the preferred path for local service boundaries. Keep runtime calls behind service bindings and keep DTOs in the owner Worker.

  • R2 is one physical bucket with multiple owners by key family. The owner of the record that references a key controls the write path and authorization policy.
  • App media reads are served by web-tanstack through /api/media/asset/* using the R2 binding. There is no signed core-worker image proxy in the app Worker path.
  • App owns user_files blob create/quota/R2 put/serve/delete, including direct uploads, saved-URL blob results, and chat-generated images. Core may scrape/probe bytes, read existing blob rows for enrichment, and enqueue workflow processing.
  • Chat writes podcast audio because podcasts are chat-owned artifacts. The app Worker only serves the bytes after checking the owning podcast row.
  • AI preflight and local cost calculation live behind the chat Worker billing facade. Subscription snapshots, idempotent ledger writes, and atomic deduction SQL live in the app Worker’s DomainRpc implementation.

These searches should stay true when changing ownership-sensitive code:

Terminal window
rg "INSERT INTO workspaces|UPDATE user_documents|INSERT INTO document_versions" workers/chat-worker/src
rg "deleteDocument|updateDocumentShare|deleteResource|removeResourceFromSource" workers/core-worker/src
rg "env\\.CORE\\.(createWorkspace|createDocument|saveDocument|readDocuments|deleteDocument)" workers/chat-worker/src
rg "env\\.DOMAIN\\.(createWorkspace|createDocumentFromMarkdown|editDocument|readDocuments)" workers/chat-worker/src

Expected result:

  • The first three commands should have no matches.
  • The last command should show chat tools calling the app Worker’s DomainRpc.

The production runtime is Cloudflare-only. External hosting integrations should not be release gates for web-tanstack; disable stale provider checks after Cloudflare Workers Builds is active.

Worker CI/CD should use Cloudflare Workers Builds, not hand-written GitHub Actions workflows. The Cloudflare Workers & Pages GitHub App is shared across Pages and Workers projects; for this repo use Workers Builds on the three existing Workers. Each connected Worker creates its own GitHub check run, and its build command is the deploy gate for PRs, preview branches, and main.

Configure Workers Builds in the Cloudflare dashboard:

Worker Root directory Production branch Build command Deploy command Non-production branch deploy command Build watch paths
newsence-core /workers/core-worker main pnpm run lint && pnpm run typecheck pnpm run deploy pnpm exec wrangler versions upload workers/core-worker/*, biome.jsonc, LICENSE
newsence-web-tanstack /web-tanstack main pnpm --dir ../workers/core-worker install --frozen-lockfile && pnpm --dir ../workers/core-worker run cf-typegen && pnpm run check && pnpm run typecheck pnpm run deploy pnpm run build && pnpm exec wrangler versions upload web-tanstack/*, workers/core-worker/*, biome.jsonc
newsence-chat /workers/chat-worker main pnpm --dir ../core-worker install --frozen-lockfile && pnpm --dir ../core-worker run cf-typegen && pnpm run lint && pnpm run typecheck pnpm run deploy pnpm exec wrangler versions upload workers/chat-worker/*, workers/core-worker/*, web-tanstack/src/server/domain/*, biome.jsonc

Set build variables per project when the package needs a pinned package manager:

  • web-tanstack: PNPM_VERSION=11.9.0
  • workers/core-worker: PNPM_VERSION=9.1.4 or upgrade the package lock/script set deliberately.
  • workers/chat-worker: PNPM_VERSION=11.9.0

Workers Builds creates GitHub check runs for each connected Worker and can create non-production preview versions/URLs. It cannot guarantee cross-Worker deploy ordering, so service-binding RPC changes should stay backward-compatible across one deploy window. If a future change truly requires atomic ordered deploys, use a temporary manual wrangler deploy sequence: workers/core-worker -> web-tanstack -> workers/chat-worker.

Keep Worker runtime secrets in Cloudflare Worker secrets / secrets.required. Do not copy app/runtime secrets into GitHub Actions just for Worker CI/CD.

Each Worker still owns its own local deploy gate. Run these before changing bindings, routes, or Worker runtime code:

Terminal window
pnpm --dir web-tanstack run deploy:dry-run
pnpm --dir workers/core-worker run deploy:dry-run
pnpm --dir workers/chat-worker run deploy:dry-run

No test framework is currently configured. Use each package’s lint/typecheck gate, then exercise the runtime path being changed. For Core ingest changes, resyncResource covers per-resource acquisition/enrichment and wrangler dev --test-scheduled covers cron discovery; neither substitutes for the other.

For startup profiling, use the local Wrangler check exposed by each package:

Terminal window
pnpm --dir web-tanstack run check:startup
pnpm --dir workers/core-worker run check:startup
pnpm --dir workers/chat-worker run check:startup

deploy:dry-run proves Wrangler can build and upload the Worker bundle shape without publishing it. check:startup is a local profiler, so treat the profile as diagnostic evidence rather than a production timing guarantee.

Production-only credentials must be declared through each Worker’s Wrangler secrets.required config rather than hidden in external project settings:

  • web-tanstack: auth, Google OAuth, and Stripe secrets. Cloudflare Email Sending uses the app Worker’s EMAIL binding instead of account-scoped API token secrets.
  • workers/core-worker: platform API secrets only; app/core calls use service-binding RPC.
  • workers/chat-worker: EXA_API_KEY for the search-web tool.

Use Cloudflare primitives where they improve the TanStack Worker:

  • Workers Static Assets: keep static build output on Cloudflare’s asset path so matching files are served without invoking app SSR. This is the replacement path for new full-stack apps, not deprecated Workers Sites.
  • Smart Placement: keep placement.mode = "smart" on Workers that perform database or service-binding work, then validate real latency with Workers metrics and browser-side telemetry. Do not assume Smart Placement helps pure static traffic.
  • Hyperdrive: keep the cache-disabled HYPERDRIVE binding for database reads and writes. Add a cached binding only after a public query path is proven identity-independent and is wired to it explicitly.
  • Workers Cache: prefer the new Worker-level cache over handwritten caches.default logic for HTTP/SSR/R2 response caching. Configure cache at the Worker or named-entrypoint level and control it with standard Cache-Control headers. Do not enable caching on the default TanStack entrypoint until auth, cookies, and private responses are audited; instead, use a small cacheable named entrypoint for public reads when a path is proven safe. Workers Cache can replace future app-level response-cache code, but it does not replace Hyperdrive query caching because it caches Worker responses, not PostgreSQL query results.
  • Service bindings / RPC: app-domain behavior stays in the app Worker, chat behavior stays in the chat Worker, and core engine behavior stays in the core Worker. This preserves the open-source core boundary without a shared package.
  • Observability: use Workers metrics for request count, errors, CPU time, wall time, execution duration, and memory percentiles after each deploy. Treat memory/isolate recycling as production telemetry, not something a local TTFB script can prove. Do not keep app-level request timing wrappers unless a concrete route-level profiling need remains; legacy hosting debug headers add code paths that Workers already covers at the runtime layer.

Cloudflare docs used for this migration checklist:

Do not add a generic shared Worker contract package. Contract ownership stays local:

  • App/domain contracts live under web-tanstack/src/server/domain or web-tanstack/src/types.
  • Chat protocol, model, podcast, and billing contracts live under workers/chat-worker/src/chat.
  • Core workflow and engine DTO contracts live under workers/core-worker/src.
  • If web and chat/core need the same wire shape, duplicate the small runtime-neutral type locally and keep behavior behind the owner Worker’s API.

Do not move product policy into shared code just to avoid duplication. Plan gates, quotas, allowed model policy, monthly credit grants, workspace limits, billing rules, and domain write behavior should live with the Worker that owns that domain. Other Workers should ask the owner through a Cloudflare service binding / RPC boundary.

This keeps the core Worker extractable as an open-source engine and keeps product policy encapsulated in the app/chat Workers instead of leaking into shared packages.

If a value answers “what does this JSON/RPC payload mean?”, it belongs in the Worker-owned public contract for that API.

If a value answers “what is this product allowed to do?”, it belongs behind the owner Worker’s API.