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.

projects.entity.ts
TypeScript
// 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.

projects.list.query.server.ts
TypeScript
// 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.

Ö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.