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.

Marfa has one SDK surface and three deployment modes. Apps use the same client code regardless of mode; only the transport and enforcement guarantees change.
Mode versus client. Sync agents, importers, CLIs, and similar automation tools are apps with credentials, not a deployment mode and not a server feature. Each runs against whichever mode its host chose. The word sync in “server-synced” describes data movement between a client and the server — it is independent of any particular client, including clients that happen to carry sync in their name.

Local-only

SDK + local SQLite store. No server. The SDK talks to an on-device store rather than a remote API. Items, edges, metadata, extensions — all persisted locally. Suitable for:
  • Single-app, single-device setups. A journal app that doesn’t need cross-app data.
  • Multi-app setups sharing a local store via OS mechanisms (iOS App Groups, macOS shared containers).
  • Apps that want to start local and optionally graduate to server-synced later.

Trade-offs

  • No conflict resolution. Single writer per store, or app-coordinated. The server’s version-conflict machinery doesn’t apply.
  • No cross-app provenance enforcement. Apps trust the local SDK. source is stamped best-effort by whichever SDK writes; there’s no server to validate it.
  • No sync. Data stays on-device. Reconciling across devices needs an external mechanism.

When to use

Prototypes, offline-first apps, privacy-first apps, apps that run disconnected for extended periods. Apps that later move to server-synced without changing their code.

Server-synced

SDK + the Marfa server. The canonical mode. Multi-tenant, multi-app, real-time via SSE, durable storage, server-stamped non-forgeable provenance, OAuth grants, audit trail, metrics.
Running more than one Marfa instance behind a load balancer has a small number of operational caveats — rate limits are per-instance, and SSE needs sticky sessions. See Scaling and operational limits below before adding a second instance.
All the mode-specific features called out across these docs (server-stamped source, SSE /events, OAuth flow, webhook delivery, cross-tenant sharing, admin metrics) apply only here.

Trade-offs

  • Requires connectivity. Without a network, apps can’t read or write unless they run their own local cache on top.
  • Introduces a server to operate. Self-hosted or managed by someone — someone is running Postgres and keeping it healthy.

When to use

The platform-thesis mode. Any time data matters across apps, devices, and time.

Third-party-synced

SDK + local SQLite + an external sync layer. The SDK reads and writes locally, same as in local-only mode. An external sync engine (CloudKit, a custom WebDAV bridge, a file-sync system) replicates the SQLite store across devices.

Trade-offs

  • No Marfa server. No cross-tenant sharing, no OAuth, no SSE. Whatever the third-party tool provides is what you get.
  • Conflict resolution is the third-party tool’s. Apps inherit CloudKit’s last-writer-wins-per-zone semantics, for example.
  • Provenance is best-effort. Same as local-only — no server to enforce source.
  • Tenancy maps to the third-party account. One iCloud account = one tenant. A user with personal and work iCloud accounts runs them as separate Marfa tenants.

When to use

Apps that want cross-device sync without standing up a Marfa server. Consumer iOS/macOS apps that lean on CloudKit for free sync. Privacy-first setups where data never leaves the user’s own cloud account.

What’s the same across modes

  • The SDK surface. client.items.create, client.edges.create, etc. are identical.
  • The data shape. Items, edges, metadata, extensions, attachments all have the same structure.
  • The type system. Core types and custom types register the same way. Inheritance works the same way.
  • The edge model. Dual-gate write semantics apply when permissions are present; otherwise trust-based (local-only, third-party-synced).

What’s mode-specific

FeatureLocal-onlyServer-syncedThird-party-synced
source non-forgeable✗ (SDK best-effort)✓ (credential-stamped)✗ (SDK best-effort)
SSE /events
OAuth grants
Webhook delivery
Audit trail
Admin metrics
Cross-tenant sharingN/A✗ (per account)
Rate limitingN/AN/A
Conflict resolutionapp-coordinatedfield-level optimistic concurrencythird-party tool’s semantics
Retention / storage tieringapp-coordinatedtier-aware, server-enforcedper the third-party tool
Works offline✓ (always local)✗ (unless local cache layered)✓ (sync on reconnect)

Server-synced: scaling and operational limits

Marfa is safe to horizontally scale behind a load balancer. Cross-instance coordination is handled through Postgres — no Redis, no separate worker tier. A few caveats remain; none block multi-instance deployments but operators should know them.
  • Webhook delivery is cluster-safe. The delivery poller uses SELECT … FOR UPDATE SKIP LOCKED to atomically claim pending rows, so a given delivery is picked up by exactly one instance. Failed instances release claims automatically after a short TTL.
  • Background jobs run once per tick cluster-wide. Trash purge, version thinning, audit cleanup, feed expiry, and event-log cleanup each run under a named Postgres advisory lock, so only one instance performs work on each tick. Other instances skip the tick cleanly.
  • API-key last_used_at is DB-enforced. The conditional UPDATE debounces writes at the database layer, so writes collapse to at most one per hour per key regardless of how many instances are handling requests.
  • Rate limits are per-instance. The rate limiter keeps counters in the server process, not Postgres. With N instances behind a load balancer, a caller limited to RATE_LIMIT_REQUESTS gets roughly N × RATE_LIMIT_REQUESTS effective requests per window. Treat the setting as a per-instance soft cap — set it conservatively and rely on your load balancer or upstream gateway for hard global limits.
  • SSE subscriptions attach to the process-local event bus. A client connected to instance A won’t see events published on instance B. Configure sticky sessions at the load balancer for the /events route (most balancers support this out of the box — AWS ALB, nginx ip_hash / sticky cookie, Cloudflare Origin-Steering: sticky, etc.) so each SSE connection stays on one instance for its lifetime.
For any deployment larger than “one instance on one host”, rolling restarts, graceful shutdown, and a shared Postgres database are all the infrastructure you need.

Choosing

  • Build against server-synced by default. It’s the mode with the most guarantees and the one most features target.
  • Add local-only as an option if your app has a meaningful offline story or a “free tier” that doesn’t need a server. Same SDK call sites; different client config.
  • Use third-party-synced when you specifically want to avoid running a server and your platform’s sync layer meets the bar.
Switching modes later is mostly a config change in the SDK. The type system, edge model, and data shape don’t change.