Two substrates run integrations: hosted (Cloudflare) and local (Node + Postgres). The choice is per-deployment, set by the operator viaDocumentation Index
Fetch the complete documentation index at: https://docs.myme.so/llms.txt
Use this file to discover all available pages before exploring further.
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 aworker_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.| Surface | Hosted (Cloudflare) | Local (Node + Postgres) | Notes |
|---|---|---|---|
| Cron precision | One-minute floor (Cron Trigger granularity) | One-minute floor (pg-boss schedule() granularity) | Equivalent. |
| Queue retry policy | Workers Queues defaults — exponential backoff, max-attempts ceiling, DLQ routing | pg-boss defaults configured to approximate the same shape | Not bit-identical but close. Operators tuning retries should consult pg-boss’s retryLimit / retryDelay rather than the Workers Queues equivalent. |
| Per-Connection state durability | Durable 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 cache | Workers KV — edge-distributed, eventual reads | Postgres row keyed on (subscription_id, delivery_id) inside the connection’s connection.runtime extension | Local 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 bindings | Control-plane Worker calls per-Integration Worker over a Cloudflare service binding | Collapses to an in-memory function call inside the server process | Not a parity issue — the seam disappears. |
| Adversarial isolation | Workers’ isolate boundary — strong isolation | worker_thread — fault isolation, not adversarial | Local 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 serialisation | Durable Object single-writer guarantee | Postgres advisory lock on hashtext(connection_id) | Equivalent semantics — at most one handler running per Connection at any given time on either substrate. |
| Resource limits | Workers’ platform-enforced CPU + memory caps | worker_thread resourceLimits — maxOldGenerationSizeMb, maxYoungGenerationSizeMb, stackSizeMb, set per integration | Tunable 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 coordination | Per-Worker — Workers’ deployment model handles this transparently | Postgres advisory locks (SELECT pg_try_advisory_lock) + pg-boss’s SELECT ... FOR UPDATE SKIP LOCKED job leasing | Local 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’sruntime_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
hostedagainst the platform’s Cloudflare account. Integrations declaringruntime_compatibility: ["hosted"]or["hosted", "local"]are eligible. - Self-hosted Marfa defaults to
localso a freshdocker compose upworks without a Cloudflare account. Operators who want Cloudflare can setMARFA_INTEGRATION_RUNTIME=hostedand 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 —localis the default) initialises pg-boss againstDATABASE_URLand registers handlers from every integration’sdist/local.js. Set the var tohostedto delegate to the Cloudflare bridge instead.- pg-boss creates its own schema (
pgbossby 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_RUNTIMEvalue selects which. POST /runtime/webhook/:connection_idis mounted publicly (no bearer token); verification happens inside the route via the subscription’s HMAC secret. The route lives only whenMARFA_INTEGRATION_RUNTIME=local; with hosted, inbound webhooks continue routing through the Cloudflare control plane.- Per-Connection state lives under the
connection.runtimereserved extension on the connection’ssystem.connectionitem. 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_threadwith 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 theIntegrationManifest, includingruntime_compatibility.worker.ts— Cloudflare Workers entry. Re-exportsPerConnectionStateplus the handlers.local.ts— local-runtime entry. Registers handlers and exports the manifest.
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.