← Back to the blog
Concepts #multi-tenant

Multi-tenant apps and per-app databases

One server, many isolated apps. How StreamHub uses apps, tenants, per-app SQLite and room prefixes to keep customers and workloads cleanly separated.

2 min read
Leer en español

Apps are the unit of isolation

In StreamHub, an app is a self-contained streaming workspace: live, webinars, security-cameras. Each app has its own config, its own S3 bucket, its own API tokens, and — importantly — its own database file. Nothing about one app's streams, recordings or credentials leaks into another.

This is what makes StreamHub multi-tenant. You can run a single server for several customers, or a single company's several departments, without them ever seeing each other's data.

The global / per-app split

State is deliberately split into two layers:

  • A global database (data/streamhub.db) holds only cross-cutting records: tenants, users, API tokens, the node registry, and a pointer to each app.
  • A per-app database (apps/<app>/app.db) owns everything app-scoped: the streams, vods and ingress_auth tables.

The split runs automatically and idempotently at boot, after a safe VACUUM INTO backup. The upshot: deleting an app is local, backing up an app is a single file, and a runaway workload in one app can't bloat another's tables.

Room prefixes tie it together

Every app declares a livekitRoomPrefix. When a LiveKit webhook fires for a room, StreamHub maps it back to the owning app by the longest matching prefix. That is how one shared SFU serves many isolated apps: the room namespace is partitioned per app.

Tenants and quotas

Above apps sit tenants — the billing/ownership boundary. A tenant owns apps, users and quotas (max concurrent streams, monthly recording minutes, storage). Quotas are enforced per tenant and surfaced as Prometheus gauges, so you can see usage against limits at a glance.

Creating an app

Everything is an API call. Creating an app is a single POST:

curl -X POST $BASE/apps -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"webinars","displayName":"Company Webinars"}'

From there you attach S3 credentials, mint per-app tokens, set the callback URL, and install plugins — all scoped to that one app.

Why it matters

Per-app isolation is not just tidiness. It means you can offer a customer their own bucket and credentials, delete their data cleanly on offboarding, and reason about blast radius: a misconfigured webhook or a filled disk hits one app, not the whole server.