Tenants leben im Schema.
Nicht in deinem Code.
Multi-Tenant-SaaS ist der Standardmodus für fast jedes B2B-Produkt — und genau dort schleichen sich die teuersten Bugs ein: die geleakte Zeile, der Cross-Tenant-Write, die Subscription, die die Daten von Kunde A an den Browser von Kunde B gepusht hat. Voltro macht Tenancy zu einer Invariante auf Schema-Ebene — ein Mixin, überall dort per Runtime erzwungen, wo gelesen und geschrieben wird.
// database/projects.entity.ts
import { id, table, text } from '@voltro/database'
import { tenant } from '@voltro/plugin-multitenancy'
export const projects = table('projects', {
id: id(),
name: text(),
})
.with(tenant()) // ← that's it
.reactive()
// Now EVERY read against this table is auto-filtered by
// ctx.request.subject.tenantId. EVERY insert auto-stamps it.
// EVERY subscription is auto-scoped. Cross-tenant writes throw
// TenantScopeViolation before they hit the DB.Tenancy-Enforcement, an sechs Stellen gleichzeitig.
Ein .with(tenant()) am Tabellen-Deskriptor schaltet all das im Gleichschritt ein. Es gibt keinen Pfad, auf dem das eine feuert und das andere nicht — sie sind dieselbe Runtime-Invariante.
tenant()-Schema-Mixin
Eine Zeile am Tabellen-Deskriptor. Fügt eine tenantId-Spalte → tenants-Referenz hinzu, scoped Reads zur Laufzeit, stempelt Writes automatisch und blockiert Cross-Tenant-Zugriffe auf SQL-Ebene.
Tenant-gescopte Subscriptions
Der Reactive-Matcher merged das Tenant-Prädikat per AND in jede Subscription. Writes von Tenant A können Subscriber in Tenant B buchstäblich nicht aufwecken — nicht wegen Polling, sondern wegen des Matching-Algorithmus.
assertOwnTenant()-Write-Guard
Für Mutations, die eine tenantId im Input entgegennehmen, gleicht ein einzeiliger Guard sie per Pattern-Matching gegen das aufgelöste Subject ab. Cross-Tenant-Impersonation zeigt sich als typisierter TenantMismatch-Fehler.
Rate-Limits pro Tenant
rateLimitPlugin scoped Buckets nach Subject ODER Tenant ODER API-Key. Ein einzelner lauter Kunde kann nicht alle anderen aushungern; ein bösartiger Tenant kann dein globales Rate-Limit nicht erschöpfen.
Billing-Meter pro Tenant
Das Entitlement-System von @voltro/plugin-billing zählt Quota pro Tenant, nicht pro Subject. Ein Stripe-Account, ein Quota-Plan, viele Nutzer — alles atomar beim Handler-Eintritt erzwungen.
Observability pro Tenant
OTel-Spans taggen jede Operation mit tenant.id. Traces sind pro Tenant. Logs sind pro Tenant. Das Dashboard zeigt Aufschlüsselungen von Latenz, Durchsatz und Kosten pro Tenant.
Die Runtime macht den AND-Merge für dich.
// queries/projects.list.query.server.ts
const route = (_, ctx) =>
database.projects.orderBy('createdAt', 'desc')
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// The runtime AND-merges eq('tenantId', subject.tenantId)
// into this query at execution time. You wrote 0 lines of
// tenant code. The wire payload to the client is filtered
// before it leaves the server.Cross-Tenant-Bugs sind auf einzigartige Weise fatal.
Ein Bug in der Warenkorbsumme ist peinlich. Ein Bug, der Tenant B die Daten von Tenant A zeigt, ist ein Compliance-Vorfall, eine E-Mail ans Board und ein Kunde, der vor deinem VP of Customer Success steht. Voltro nimmt das Muster "pro Projekt von Hand gebaut" vom Tisch — es gibt nichts zu vergessen.
Zwei Fehlermodi, die Voltro beseitigt:
- • Fehlende WHERE-Klausel. Eine Custom-Query in irgendeinem vergessenen Handler filtert nicht nach Tenant. Voltro: Die Runtime merged das Prädikat zur Ausführungszeit per AND. Unmöglich zu vergessen.
- • Blind vertrauter Input. Ein Handler reicht die vom Client gelieferte tenantId direkt an die DB durch. Voltro: assertOwnTenant weist Mismatches mit einem typisierten Fehler ab, bevor der Write landet.
Tenancy zieht sich durch jedes andere Primitiv.
Schema-DSL
tenant() ist einer von drei Core-Mixins (audit, tenant, softDelete). Sie komponieren mit deinen eigenen Custom-Mixins.
Reaktive Queries
Subscriptions werden vom Matcher selbst Tenant-gescopt — Cross-Tenant-Writes wecken sie nicht, unabhängig von der Quelle.
Typsicherheit
TenantMismatch ist ein typisierter Effect.fail-Wert — der Client matcht ihn per Pattern-Matching wie jeden anderen Fehler.
Öffne das Framework. Schau es dir selbst an.
Jede Primitive auf dieser Seite ist heute im Framework. Klone den Starter, lass `voltro dev` laufen, in zwei Minuten ist es auf dem Bildschirm.