Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.myme.so/llms.txt

Use this file to discover all available pages before exploring further.

Two substrates run integrations: hosted (Cloudflare) and local (Node + Postgres). The choice is per-deployment, set by the operator via MARFA_INTEGRATION_RUNTIME — it’s not per-Connection and it’s not per-Integration. Integration code (the handlers.ts module) is portable: the same code runs on either substrate without modification. Where they differ is the operational shape underneath the SDK. The manifest’s runtime_compatibility enum also accepts self-hosted as a third tier. No wired substrate currently dispatches against it; see Integrations for the manifest-level vocabulary.

The two substrates

Hosted — Cloudflare

The default for production Marfa. Each integration deploys as a Cloudflare Worker. Per-Connection state lives in a Durable Object. Scheduled polls fire from Cron Triggers; reactive runs and inbound webhooks flow through Cloudflare Queues. Inbound webhook receipts hit a single control-plane Worker (@withmarfa/runtime-control) which verifies the signature and routes onto the right per-Integration queue. Set with MARFA_INTEGRATION_RUNTIME=hosted. Requires CLOUDFLARE_QUEUES_REACTIVE_RUN_URL + CLOUDFLARE_QUEUES_API_TOKEN for the reactive-run bridge to actually fire (otherwise the bridge boots in disabled mode and only schedule + inbound-webhook traffic reaches the Workers).

Local — Node + Postgres

The substrate for self-hosters who don’t want a Cloudflare account in their stack. The same handler code runs in a worker_thread pool inside the Marfa server process; per-Connection state lives in the connection’s connection.runtime extension; pg-boss drives the cron and queue layers off the Postgres instance the rest of the server already uses. Inbound webhooks hit POST /runtime/webhook/:connection_id on the Marfa server directly — same verification adapters (@withmarfa/webhooks) the hosted substrate uses, just no remote hop. Set with MARFA_INTEGRATION_RUNTIME=local. Requires Postgres — SQLite self-hosts must keep =hosted until they migrate. Operator footprint is two containers: server + Postgres. No Redis, no extra binary, no Postgres extension dependency.

Where they differ — semantic parity sheet

The substrates are designed for parity at the handler-author surface. The differences below live underneath the SDK and rarely surface to integration code; they exist as documented operational realities rather than gotchas.
SurfaceHosted (Cloudflare)Local (Node + Postgres)Notes
Cron precisionOne-minute floor (Cron Trigger granularity)One-minute floor (pg-boss schedule() granularity)Equivalent.
Queue retry policyWorkers Queues defaults — exponential backoff, max-attempts ceiling, DLQ routingpg-boss defaults configured to approximate the same shapeNot bit-identical but close. Operators tuning retries should consult pg-boss’s retryLimit / retryDelay rather than the Workers Queues equivalent.
Per-Connection state durabilityDurable Object storage (eventually consistent)Postgres rows under transactions (strict consistency)Local is stronger. A handler that wrote a cursor and then crashed sees the write on retry — both substrates honour that.
Idempotency cacheWorkers KV — edge-distributed, eventual readsPostgres row keyed on (subscription_id, delivery_id) inside the connection’s connection.runtime extensionLocal has strict consistency by virtue of Postgres. Hosted has eventual reads but the 3600s TTL is wide enough that the eventual-consistency lag is invisible in practice.
Service bindingsControl-plane Worker calls per-Integration Worker over a Cloudflare service bindingCollapses to an in-memory function call inside the server processNot a parity issue — the seam disappears.
Adversarial isolationWorkers’ isolate boundary — strong isolationworker_thread — fault isolation, not adversarialLocal self-host treats operator-installed integrations as trusted (same posture as installing an npm package in the host app). For untrusted code, run hosted.
Per-Connection serialisationDurable Object single-writer guaranteePostgres advisory lock on hashtext(connection_id)Equivalent semantics — at most one handler running per Connection at any given time on either substrate.
Resource limitsWorkers’ platform-enforced CPU + memory capsworker_thread resourceLimitsmaxOldGenerationSizeMb, maxYoungGenerationSizeMb, stackSizeMb, set per integrationTunable on local; fixed on hosted. Defaults are tuned for the in-tree integrations and operators can lift them per-deployment if a heavier handler lands.
Multi-instance coordinationPer-Worker — Workers’ deployment model handles this transparentlyPostgres advisory locks (SELECT pg_try_advisory_lock) + pg-boss’s SELECT ... FOR UPDATE SKIP LOCKED job leasingLocal is multi-instance safe out of the box. The reactive-run bridge runs under a single advisory lock so two server instances never fan out the same event twice.

Picking a substrate

You don’t pick per-Integration — the manifest’s runtime_compatibility field declares which substrates an integration supports, and the operator’s MARFA_INTEGRATION_RUNTIME setting picks which substrate the deployment runs.
  • Production-managed Marfa runs hosted against the platform’s Cloudflare account. Integrations declaring runtime_compatibility: ["hosted"] or ["hosted", "local"] are eligible.
  • Self-hosted Marfa defaults to local so a fresh docker compose up works without a Cloudflare account. Operators who want Cloudflare can set MARFA_INTEGRATION_RUNTIME=hosted and wire the Cloudflare bridge env vars; integrations declaring ["hosted"] then become eligible.
  • The sync integration declares runtime_compatibility: ["local"] only — it has no hosted path.

Local substrate operator notes

  • MARFA_INTEGRATION_RUNTIME=local (or the env var unset — local is the default) initialises pg-boss against DATABASE_URL and registers handlers from every integration’s dist/local.js. Set the var to hosted to delegate to the Cloudflare bridge instead.
  • pg-boss creates its own schema (pgboss by default) inside the same Postgres instance. No extra database, no extra credential.
  • The reactive-run bridge subscribes to the in-process pubsub — the same firehose the Cloudflare bridge consumes when running hosted. One substrate runs per server; the MARFA_INTEGRATION_RUNTIME value selects which.
  • POST /runtime/webhook/:connection_id is mounted publicly (no bearer token); verification happens inside the route via the subscription’s HMAC secret. The route lives only when MARFA_INTEGRATION_RUNTIME=local; with hosted, inbound webhooks continue routing through the Cloudflare control plane.
  • Per-Connection state lives under the connection.runtime reserved extension on the connection’s system.connection item. The hosted substrate uses the same namespace, so handler-visible state has the same shape on either substrate.
  • Workers crash → pool replaces. A handler that throws repeatedly takes its worker_thread with it; the next dispatch lands on a fresh thread. Other integrations’ pools are untouched.

Authoring against either substrate

The handler module is the same on both substrates. Per-substrate divergence lives in the entry files:
  • handlers.ts — registers schedule, webhook, and item-event handlers. Substrate-agnostic.
  • manifest.ts — exports the IntegrationManifest, including runtime_compatibility.
  • worker.ts — Cloudflare Workers entry. Re-exports PerConnectionState plus the handlers.
  • local.ts — local-runtime entry. Registers handlers and exports the manifest.
Both entry files call registerHandlers() from the integration’s own handlers.ts, which seeds @withmarfa/runtime-sdk’s in-process handler registry. The substrate then dispatches against that registry. Substrate-specific concerns — Durable Object storage proxies, worker_thread boot, queue wiring — sit outside the handler module entirely. Importing @withmarfa/runtime-sdk/cloudflare from inside handlers.ts defeats portability. Substrate-specific imports belong in worker.ts.