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.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.
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.
sourceis 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. All the mode-specific features called out across these docs (server-stampedsource, 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
| Feature | Local-only | Server-synced | Third-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 sharing | N/A | ✓ | ✗ (per account) |
| Rate limiting | N/A | ✓ | N/A |
| Conflict resolution | app-coordinated | field-level optimistic concurrency | third-party tool’s semantics |
| Retention / storage tiering | app-coordinated | tier-aware, server-enforced | per 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 LOCKEDto 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_atis 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_REQUESTSgets roughlyN × RATE_LIMIT_REQUESTSeffective 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
/eventsroute (most balancers support this out of the box — AWS ALB, nginxip_hash/sticky cookie, CloudflareOrigin-Steering: sticky, etc.) so each SSE connection stays on one instance for its lifetime.
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.