Server
Storage
Database
SQLite is perfectly fine for single-tenant self-hosts. Postgres is recommended for multi-tenant, multi-app, high-traffic deployments.
Blobs
Cloudflare R2 example
R2 is S3-compatible. Use the dedicated R2 access key (Cloudflare dashboard → R2 → Manage API Tokens), the bucket’s account-scoped endpoint, and any region string — R2 ignores the region but the SDK requires one (auto is the convention).
Retention and lifecycle
Authentication (Better Auth + OAuth)
Account lifecycle
User-initiated account deletion runs a grace-window cascade. See Account lifecycle for the flow; the levers below tune timing.Email transport
Transactional email (sign-up verification, forgot-password, account-deletion confirm + cancel). Default is unconfigured — email-dependent flows return HTTP503 email_transport_not_configured until an operator picks a backend.
The Cloudflare backend has no documented send-time idempotency header — a transient retry can produce duplicate deliveries. Acceptable for the transactional flows wired today (each ships a single-use token; a second send is semantically harmless).
Per-tenant quotas
Default ceilings applied to every tenant. Per-tenant overrides intenant_quotas rows take precedence; unset = unlimited (no enforcement).
Errors carry
details: { resource, limit, current } so SDK / CLI / operator alerts can wire off the shape.
Connections runtime (Cloudflare control plane)
The Connections build runs Integration Workers on Cloudflare. The server reaches them through these.Observability
Structured logs are always emitted to stdout as JSON lines. Each log carries a UUIDv7
request_id that’s also returned in the X-Request-Id response header.
GET /health returns database and blob-backend reachability with latency metrics. GET /metrics (admin) returns per-tenant counts and rates.
Minimal self-host
The smallest working server:./data/blobs, rate-limited at 1000 req/min per credential. Bootstrap the first admin key:
key field from the response. That’s your admin credential.
Production-shape self-host
Reverse proxy
If the server sits behind a reverse proxy, the proxy must pass through:- The
Authorizationheader. - The raw request body (unbuffered) for webhook signature verification and SSE streaming.
text/event-streamresponses without buffering. Disable response buffering for/events.
TRUSTED_PROXY_CIDRS to the CIDR of your proxy so the rate limiter and any future per-IP logic see the real client IP rather than the proxy’s address. Without it, x-forwarded-for is ignored as a precaution against spoofing.
Migration scripts
One-shot scripts that ride alongside the server, used during upgrades. They take their own environment variables — set them only when running the script, not on the long-running server process.
Run with
pnpm --filter @withmarfa/server migrate:sync-json-to-connection. See the script source in packages/server/src/scripts/ for invocation details.
What is not configurable
- Core types. Registered at startup from the bundled registry.
- Reserved namespaces.
core.*,system.*, andmarfa.*are hard-coded and only writable by platform-flagged credentials.