Worker boundaries
The Cloudflare migration splits the runtime into three Workers:
web-tanstackis 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-workeris the chat/agent Worker. It owns chat sessions, AI execution, tool orchestration, podcast artifacts, and AI billing enforcement.workers/core-workeris the engine Worker. It owns scraping, ingestion, AI Search synchronization, entity extraction, queues, and Workflows.
Worker Matrix
Section titled “Worker Matrix”| 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 |
Service Binding Contracts
Section titled “Service Binding Contracts”- Core service DTOs live with the engine methods that own them, primarily
workers/core-worker/src/corpus.tsandworkers/core-worker/src/okf.ts. DomainRpcDTOs (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_WORKERwith the user identity header. - The app Worker calls Core through
CORE_WORKER; Chat calls Core throughCORE; Chat and Core call the app-ownedDomainRpcthroughDOMAIN. - Core exposes only
/healthoverfetch. Ingest, search, export, and workflow operations are service RPC, scheduled handlers, or Workflow entrypoints rather than internal-token HTTP endpoints.
Service Binding Typing Direction
Section titled “Service Binding Typing Direction”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:
pnpm exec wrangler types -c wrangler.jsonc -c ../workers/core-worker/wrangler.jsoncThe chat worker does the same for the core binding:
pnpm exec wrangler types -c wrangler.jsonc -c ../core-worker/wrangler.jsoncThat 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.
Current Data Ownership
Section titled “Current Data Ownership”- 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-tanstackthrough/api/media/asset/*using theR2binding. There is no signed core-worker image proxy in the app Worker path. - App owns
user_filesblob 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
DomainRpcimplementation.
Boundary Checks
Section titled “Boundary Checks”These searches should stay true when changing ownership-sensitive code:
rg "INSERT INTO workspaces|UPDATE user_documents|INSERT INTO document_versions" workers/chat-worker/srcrg "deleteDocument|updateDocumentShare|deleteResource|removeResourceFromSource" workers/core-worker/srcrg "env\\.CORE\\.(createWorkspace|createDocument|saveDocument|readDocuments|deleteDocument)" workers/chat-worker/srcrg "env\\.DOMAIN\\.(createWorkspace|createDocumentFromMarkdown|editDocument|readDocuments)" workers/chat-worker/srcExpected result:
- The first three commands should have no matches.
- The last command should show chat tools calling the app Worker’s
DomainRpc.
Cloudflare Deployment Checks
Section titled “Cloudflare Deployment Checks”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.0workers/core-worker:PNPM_VERSION=9.1.4or 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:
pnpm --dir web-tanstack run deploy:dry-runpnpm --dir workers/core-worker run deploy:dry-runpnpm --dir workers/chat-worker run deploy:dry-runNo 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:
pnpm --dir web-tanstack run check:startuppnpm --dir workers/core-worker run check:startuppnpm --dir workers/chat-worker run check:startupdeploy: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’sEMAILbinding 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_KEYfor the search-web tool.
Cloudflare-Native Migration Levers
Section titled “Cloudflare-Native Migration Levers”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
HYPERDRIVEbinding 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.defaultlogic for HTTP/SSR/R2 response caching. Configure cache at the Worker or named-entrypoint level and control it with standardCache-Controlheaders. 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:
- Cloudflare CI/CD overview: https://developers.cloudflare.com/workers/ci-cd/
- Workers Builds overview: https://developers.cloudflare.com/workers/ci-cd/builds/
- Workers Builds GitHub integration: https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/github-integration/
- Workers Builds configuration: https://developers.cloudflare.com/workers/ci-cd/builds/configuration/
- Workers Builds monorepo setup: https://developers.cloudflare.com/workers/ci-cd/builds/advanced-setups/
- Workers Builds branch control: https://developers.cloudflare.com/workers/ci-cd/builds/build-branches/
- Workers Builds watch paths: https://developers.cloudflare.com/workers/ci-cd/builds/build-watch-paths/
- Wrangler commands with pnpm: https://developers.cloudflare.com/workers/wrangler/commands/
- TanStack Start on Workers: https://developers.cloudflare.com/workers/framework-guides/web-apps/tanstack-start/
- Workers Static Assets: https://developers.cloudflare.com/workers/static-assets/
- Workers Cache launch: https://blog.cloudflare.com/workers-cache/
- Workers Cache examples: https://developers.cloudflare.com/workers/cache/examples/
- Smart Placement: https://developers.cloudflare.com/workers/configuration/placement/
- Hyperdrive query caching: https://developers.cloudflare.com/hyperdrive/concepts/query-caching/
- Workers metrics and memory: https://developers.cloudflare.com/workers/observability/metrics-and-analytics/
Local Contract Policy
Section titled “Local Contract Policy”Do not add a generic shared Worker contract package. Contract ownership stays local:
- App/domain contracts live under
web-tanstack/src/server/domainorweb-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.
Practical Rule
Section titled “Practical Rule”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.