Skip to content

Worker boundaries

Newsence runs three production Workers with deliberately different authority.

Worker Owns Calls Does not own
web-tanstack Product UI, authentication, DomainRpc, Postgres product transactions, user files and product R2 lifecycle CORE_WORKER, THINK_WORKER Agent transcripts, model execution, acquisition and enrichment
workers/think-worker Think Agent sessions, AI execution, tool policy, native Workspace scratch files, podcast orchestration DOMAIN Product SQL, product R2 authority, public-corpus indexing
workers/core-worker Acquisition, enrichment, AI Search, entities, queues and Workflows APP for narrow app-owned lifecycle operations Product workspace/document policy, Agent transcripts, subscriptions

The browser reaches Think through the authenticated app proxy. Think calls the app-owned DomainRpc for product reads and writes. The app and Core communicate through a service binding. Core’s public fetch surface remains intentionally unavailable; engine operations are RPC, scheduled handlers, queues, or Workflows.

Product knowledge is not a filesystem mount:

  • public discovery uses Core-owned AI Search;
  • document/resource reads and workspace source listing are authorized by the app Worker on every call;
  • Think exposes this as one bounded knowledge tool;
  • Native Workspace owns only session-local scratch files in DO SQLite.

Postgres remains canonical for documents, resources, memberships, podcasts, quotas, and session catalog rows. R2 remains canonical for product binary objects. Native Workspace files are working state and can only become product state through an explicit app-domain action.

bash operates on a virtual filesystem rooted at /. Code Mode’s execute runs in a Worker isolate, with optional Chromium/CDP access through the Browser Run binding. This Worker has no Container or Sandbox binding.

Cloudflare service bindings are the runtime boundary. Do not introduce a generic cross-worker contracts package.

  • Think protocol, tool, podcast, and runtime DTOs live under workers/think-worker/src.
  • Core engine and workflow DTOs live under workers/core-worker/src.
  • Generated Service<> bindings come from multi-config wrangler types when the target entrypoint can be loaded under the caller’s TypeScript project.
  • Otherwise the caller refines only its consumed methods with Rpc.Provider, and the receiving Worker validates untrusted payloads at runtime.

Do not import source files across Worker directories. Product policy and exact input validation remain behind the owner Worker’s RPC implementation.

Keep the user’s request, the saved resource, and its execution tasks separate:

Concept Authority and durable state
Import Web owns the user’s intent, destination, admission policy, and retry decision. Its user-scoped ResourceProgress DO retains the admission receipt, current attempt, and subscriptions.
Resource Core owns canonical identity, classification, content, and normalized persistence in Postgres. Web owns raw-file ownership, profile saves, placements, and canonical attachment.
Task Core owns execution, provider retries, and Workflow/queue lifecycle. Web’s ResourceTaskProgress DO retains the progress projection and shares it with subscribing imports.

Processing state stays in DO storage; Resource and UserFile do not carry job status. A readable original can be ready while translation, references, or search indexing continue as separate tasks. For managed imports, the Task DO also guards execution admission: Core must claim the current task before starting provider work.

New URLs enter Core as immutable drafts. Core processes the draft before creating or merging its canonical row, then calls Web to apply the user’s save target. An already-published URL can be saved immediately. Uploaded PDFs first establish the raw resource, ownership, and placement in one Web transaction. That transaction must also obtain durable DO admission before committing; dispatch waits until ownership is committed. A new PDF admission failure rolls back the upload, while a failed retry of an existing PDF preserves the readable file.

Maintain the flow at these entry points:

  • Web admission: web-tanstack/src/server/resource-ingest.server.ts owns URL and PDF admission, fallback grants, receipt outcomes, operator retries, and completion checks. Web’s src/server/resource-ingest.functions.ts, src/routes/api.upload.ts, and src/server/domain-rpc.ts adapt authenticated requests and RPC calls. Resolve fallback grants before taking database locks.
  • Storage and saves: web-tanstack/src/server/user-file-persistence.server.ts owns byte validation, quota, deduplication, and object-storage lifecycle. It calls admission inside the ownership transaction. web-tanstack/src/server/resource-write.server.ts owns the Postgres writes; keep DO scheduling out of its transaction helpers. Preserve the current-attempt check before applying a Core completion result.
  • Execution and progress: workers/core-worker/src/ingest/workflow.ts owns processing order; workers/core-worker/src/ingest/progress.ts owns task reporting and execution policy. web-tanstack/src/server/resource-progress.server.ts implements both DOs, durable dispatch, subscriptions, and authenticated progress delivery. Keep physical tasks shareable across imports.
  • Frontend: web-tanstack/src/components/workspace/resourceImport.ts owns submission, per-item outcomes, retry input, and mutation side effects. ResourceProgressProvider.tsx in that directory owns the progress connection, snapshot store, and progress UI. web-tanstack/src/lib/queries/resources.ts owns cache invalidation. Each dialog retains its own draft and pending state.

Import activity and warnings reflect processing and translation. Indexing and reference enrichment run in the background; their existing subscriptions deliver completion notifications for cache refresh, including after an import settles. They do not report running progress or keep the import active. Their terminal reports seal the leaf task atomically, without a second RPC. Parent enrollment still protects the handoff to user-visible work. Deploy Web before Core so the receiving progress contract is available first.

Every shared translation registers its Task DO before Workflow admission, even when it starts from background monitoring without a subscriber. Later imports subscribe to that same ledger. Progress reports run in required durable steps; Web unavailability delays translation through step retries. Automatic acquisition can run without a progress ledger, but translation always uses one. There is no native-status reconciliation or reader polling. Native Workflow status is consulted only to deduplicate admission and select retry generations, with completed results retained for seven days. Resource reads return persisted content; socket completion notifications refresh affected caches, including when an initial read was already in flight. Closing processing enrollment also refreshes content to cover a shared translation that completed before subscription. Both progress DOs remain in Web.

Background subject discovery uses a separate Workflow identity from user-owned subject processing. Both still converge on the same canonical resource and shared translation identity. Concurrent first acquisitions may duplicate provider work; this prevents a background task from silently inheriting a user’s progress graph.

Add platform acquisition and normalization in workers/core-worker/src/ingest/platforms/, with detection dispatch in workers/core-worker/src/ingest/acquisition.ts. The Web admission flow stays platform-independent and consumes persisted Core facts. A new provider should not require another frontend or Web processing pipeline.

These searches should remain empty when changing authority-sensitive code:

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

Think product mutations should call app-owned methods through env.DOMAIN. Core should not acquire product write authority merely because it can read the same Postgres cluster.

Each package owns its install, type generation, lint/typecheck, build, and deploy commands. Cloudflare Workers Builds should use these roots:

Worker Root Production gate Deploy
newsence-web-tanstack /web-tanstack pnpm run typecheck && pnpm run build pnpm run deploy
newsence-think /workers/think-worker pnpm lint && pnpm run typecheck pnpm run deploy
newsence-core /workers/core-worker pnpm run typecheck && pnpm lint pnpm run deploy

Service-binding changes should normally be additive for one deploy window. A deliberately breaking release requires an explicit maintenance window and a documented target/consumer order. Never assume independent Workers Builds will deploy in a coordinated order.

Breaking Think action changes retire existing chats through the session deletion ledger. The maintenance cron schedules native DO destruction; documents and generated images remain. Do not translate old action keys or resume old turns.

During a coordinated cutover, chat_sessions_action_cutover temporarily blocks new chat allocation. Apply its removal only after Web and Think are deployed:

  1. From web-tanstack, export the direct production database as DIRECT_DATABASE_URL, then run pnpm db:migrate:deploy.
  2. In a controlled CI or operator environment, run pnpm db:drift-check with a separate disposable SHADOW_DATABASE_URL. The audit verifies both the migration ledger and drift, including partial indexes, column-list foreign keys, views, functions, and triggers that Prisma Schema Language cannot represent.
  3. Deploy web-tanstack, then workers/think-worker, then Core when changed. The app is Think’s service-binding callee and must be available first.
  4. Apply the cutover-release migration to reopen chat allocation. Confirm the old session deletion ledger has drained before ending maintenance. Run drift and runtime checks when authorized; deployment success alone does not verify application behavior.

Secrets stay in Cloudflare Worker secrets or bindings. Think owns its model bindings and provider routing; the app owns auth, billing, email, and product-storage credentials; Core owns ingest-platform credentials.

If a value answers “what does this RPC payload mean?”, it belongs with the API owner’s contract. If it answers “what is this product operation allowed to do?”, it belongs behind the owner Worker rather than in shared code.