Skip to content

Multi-tenant mode

Set TENANT_MODE=multi to host many isolated vaults on one process. Each user gets {slug}.yourservice.com, their own rows in shared SQLite (scoped by tenant_id), and optional Stripe billing.

This powers syncbins.com. Most self-hosters should keep TENANT_MODE=single.

  • Subdomain routingHost: slug.yourservice.com resolves to a tenant row.
  • Public signupPOST /api/signup on the apex domain creates a tenant and returns the tenant URL.
  • On-demand TLS — Caddy asks GET /api/tls-check?domain=… before issuing a cert for a subdomain.
  • Stripe (optional)STRIPE_BILLING_ENABLED=true activates checkout on signup and webhooks for subscription state.
  • Per-tenant storage accountingstorage_bytes on the tenant row; enforce caps with STORAGE_CAP_BYTES.
  • Multi-user inside one tenant — still one recovery phrase per subdomain.
  • Cross-tenant sharing.
  • Compliance packaging (GDPR tooling, audit logs) — you operate the service.
slug.yourservice.com ──► Caddy (TLS) ──► Fastify SyncBins :8080
SQLite (one file)
tenant_id on all rows
R2 / Azure / local blobs

Unlike some SaaS designs, tenants share one SQLite database with a tenant_id column — not separate db.sqlite files per slug. Blob paths are scoped by storage adapter configuration.

Conceptual comparison: Multi-tenant vs single.

  1. DNS — Wildcard *.yourservice.com and apex yourservice.com → your server IP.

  2. Caddyfile — Use on-demand TLS with the ask hook (bundled example in App/docker/Caddyfile):

    {
    on_demand_tls {
    ask http://syncbins:8080/api/tls-check
    interval 2m
    burst 5
    }
    }
    *.{$DOMAIN}, {$DOMAIN} {
    tls { on_demand }
    reverse_proxy syncbins:8080
    }
  3. Environment — in docker/.env:

    DOMAIN=yourservice.com
    TENANT_MODE=multi
    BASE_DOMAIN=yourservice.com
    BLOB_URL_SECRET=…
    STORAGE_KIND=local # or r2 / azure
    # Optional billing
    STRIPE_BILLING_ENABLED=true
    STRIPE_SECRET_KEY=sk_live_…
    STRIPE_WEBHOOK_SECRET=whsec_…
    STRIPE_PRICE_ID=price_…
  4. Start composedocker compose up -d from App/docker/.

  5. Create a tenant — signup via UI on apex or API:

    Terminal window
    curl -X POST https://yourservice.com/api/signup \
    -H "Content-Type: application/json" \
    -d '{"email":"you@example.com","subdomain":"gavin"}'

    Response includes the tenant URL (e.g. https://gavin.yourservice.com). Open it and complete onboarding as the first device.

  6. TLS check — first visit to https://gavin.yourservice.com triggers Caddy → /api/tls-check?domain=gavin.yourservice.com → 200 only if tenant exists.

When STRIPE_BILLING_ENABLED=true:

  • Signup may redirect through Stripe Checkout before the tenant is active.
  • Webhook: POST /api/billing/webhook (configure in Stripe dashboard).
  • Customer portal: GET /api/billing/portal from a paired device.

Inactive tenants past grace return 402 payment_required on API routes (see tenant middleware).

Exact event list and suspension timing — verify in App/server/src/routes/ before production; do not rely on undocumented grace periods.

  • Backups: Snapshot the whole SQLite file + blob store — all tenants together. See Backups.
  • Logs: docker compose logs syncbins — include tenant_id from request context when debugging.
  • Quota: Set STORAGE_CAP_BYTES per deployment; 0 = unlimited (self-host default).