Batterien inklusive.
Ein Vertrag, 40+ Plugins.

Der Stack, den eine echte SaaS braucht — Billing, Storage, Mail, Notifications, Search, Analytics, Rate-Limiting — als First-Party-Plugins über einen einzigen Vertrag. Jedes kann Handler intercepten, das Schema erweitern, Routes ergänzen oder Lifecycle-Hooks laufen lassen. Installiere nur, was du brauchst; sie komponieren.

plugins.ts
TypeScript
// definePlugin — one contract for every extension
import { definePlugin } from '@voltro/protocol'

export const billing = () => definePlugin({
  name: 'billing',
  extendSchema:      { tables: [subscriptions, plans] },
  interceptMutation: guardEntitlements,
  routes:            [checkoutRoute],
})

// install only what you need — they compose
plugins: [billing(), storage(), mail(), notifications()]

Das Ökosystem, nach Aufgabe.

Ein Plugin-Vertrag

definePlugin lässt ein Plugin Mutations / Queries / Actions intercepten, das Schema erweitern (Tabellen + Migrationen oder ein Mixin), Effect-Service-Layer bereitstellen, Routes und Inspect-Endpunkte registrieren und Install- / Activate-Lifecycle-Hooks laufen lassen. Die Reihenfolge ist explizit; Plugins komponieren outer→inner.

Commerce & Billing

plugin-billing modelliert Subscriptions, Pläne, Entitlements und Usage-Metering über einen Provider (Stripe + Mock) mit Seats, Proration, Dunning und einem requireEntitlement()-Guard. plugin-licensing verifiziert offline EdDSA-Lizenzschlüssel.

Storage, Mail & Notifications

plugin-storage (S3 — R2/GCS via S3-Interop — Azure / database / Filesystem) mit Presigned URLs und Image-Transforms; plugin-mail (Resend / Postmark / SES / SMTP) mit React-Email-Templates; plugin-notifications über E-Mail / Slack / SMS / Mobile Push / Web Push (VAPID) / In-App.

Daten- & Schema-Mixins

audit(), softDelete(), deactivation(), Row-History + Time-Travel, Governance (Retention, GDPR-Export/-Löschung, Feld-Verschlüsselung) und Multitenancy — Querschnittsbelange als komponierbare Mixins.

Search, Analytics & Webhooks

plugin-search synct zu Typesense / Meilisearch / Algolia; fünf Analytics-Sinks (Postgres, DuckDB, ClickHouse, Tinybird, PostHog) hinter einem ctx.analytics; plugin-webhooks macht durable Outgoing + verifiziertes Incoming (Stripe- / GitHub- / Slack-Presets).

Kontrolle & Betrieb

plugin-ratelimit (fixed / sliding / Token-Bucket), plugin-flags (Targeting + %-Rollout + Kill-Switch), plugin-moderation, plugin-rbac, plugin-openapi und die Observability-Exporter (Prometheus, Datadog, Sentry, Log-Ship).

Kommentar-Threads

plugin-comments hängt Threads an jeden Anker, den deine App benennen kann — eine Bestellung, ein Dokument, einen Abschnitt. Der Zugriff delegiert an deinen eigenen Guard und fällt closed, solange keiner deklariert ist; Mentions werden beim Autocomplete auf den Mandanten des Aufrufers gefiltert und beim Anlegen erneut validiert, keine Benachrichtigung überquert die Grenze. Reactions, Unread pro Subject und Resolve/Reopen reiten auf demselben reaktiven Kanal: useComments unter einem ejectbaren CommentsThread.

Füg nur hinzu, was du brauchst.

Komponierbar, kein Monolith

Eine Voltro-App installiert die Plugins, die sie nutzt, und sonst nichts — jedes ist ein Paket über denselben Vertrag, also teilen sie Runtime, Change-Feed und Migrations-Planer. Ein paar Plugins füllen sich noch: plugin-cdc-out shippt seine Engine plus Memory-, Webhook- und — via plugin-queue — Kafka-Sinks; ein Warehouse ist ein CdcSink, den du selbst implementierst, nicht schlüsselfertig. Und plugin-queue selbst ist die Kafka-Interop-Tür: Schema-dekodierte Consumer gegen deine bestehenden Topics, transaktionales Produzieren über die Outbox.

Die Plugin-Oberfläche, gezählt.

45

First-Party-Plugins, jedes zuschaltbar und erst installiert, wenn eine Anforderung kommt

80

veröffentlichte Pakete insgesamt — ein Plugin ist ein ganz gewöhnliches davon, ohne privilegierte API

Wie das Installieren eines Plugins aussieht.

app.config.ts
ts
// app.config.ts
import { defineApp } from '@voltro/runtime'
import { authPlugin } from '@voltro/plugin-auth'
import { storagePlugin } from '@voltro/plugin-storage'

export default defineApp({
  name: 'acme',
  plugins: [
    authPlugin({ mfa: true, passkeys: true }),
    storagePlugin({ provider: 's3', bucket: 'acme-uploads' }),
  ],
})

Ein Plugin deklariert, was es braucht und was es beiträgt; der Boot-Audit verweigert eines, das über seine Deklaration hinausgreift. Ein eigenes zu schreiben hat dieselbe Form wie eines zu nutzen — es gibt keine separate Autoren-API zu lernen.

Der Plugin-Contract im Detail.

Was kann ein Voltro-Plugin tatsächlich?

Ein Plugin ist eine serverseitige Erweiterung, die sich in die Runtime hineinkomponiert, statt daneben zu stehen. Es kann jede Mutation, Query und Action abfangen; Tabellen, Schema-Mixins und Migrationen beisteuern; rpc-Routen und rohe HTTP-Endpunkte mounten; eine Effect-Service-Schicht bereitstellen, die jeder Handler erreicht; und Install-, Activate-, Deactivate- und Uninstall-Lifecycles fahren.

Diese Liste ist bewusst endlich. Ein Plugin kann das Kern-Schema-DSL und den Query-Builder nicht verändern, Tenant-Scoping nicht umgehen und keine Secrets lesen, die es nicht bekommen hat — seine Konfiguration kommt über die eigene Factory-Funktion, die berührte Oberfläche ist also die deklarierte.

Das Framework liefert über vierzig First-Party-Plugins über genau diesen einen Contract: Auth, Billing, Storage, Mail, Suche, Moderation, Governance, Analytics-Sinks, Observability-Exporter, Identity-Adapter. Sie sind nicht privilegiert. Alles, was sie tun, kann ein Plugin von dir auch — genau die Eigenschaft, die es lohnend macht, den Contract einmal zu lernen.

Wie komponieren Interceptors, und zählt die Reihenfolge?

Interceptors umschließen die Procedure von außen nach innen in der Reihenfolge, in der du die Plugins listest. Die Reihenfolge zählt und ist sichtbar: setz einen Rate-Limiter vor ein Audit-Log, und ein abgelehnter Request erreicht die Audit-Spur nie — meist genau das, was du willst, und immer etwas, das man an einer Stelle sehen können sollte, statt es aus dem Verhalten zu erschließen.

Ein Interceptor sitzt innerhalb der Garantien des Frameworks, nicht darum herum. Er läuft, nachdem die Runtime das Tenant-Prädikat gemerged hat — ein Mutation-Interceptor kann die Zeilen eines anderen Mandanten also nicht lesen, indem er das Subject umschreibt. Zugriffskontrolle ist nichts, was ein Plugin versehentlich lockern kann.

Weil Interception ein Wrap ist und kein Event, kann ein Plugin Input transformieren, Output transformieren, mit einem getypten Fehler kurzschließen oder Telemetrie um den Aufruf legen — ohne dass der Handler weiß, dass es ein Plugin gibt.

Wie verhalten sich Plugin-Tabellen zu meinen eigenen?

Ein tabellenführendes Plugin exportiert seine Table-Handles — eine Spalte in deinem Schema kann eine davon also genau so referenzieren wie deine eigenen: `reference(() => aiFlowsTable, …)` erzeugt einen echten Fremdschlüssel mit derselben On-Delete-Semantik und denselben Index-Defaults. Die Datenbank erzwingt die Verknüpfung, es gibt also keinen Cleanup-Subscriber, den man vergessen könnte.

Die Tabelle als WERT zu referenzieren statt ihren Namen als String ist das, was das über die Migrationen des Plugins hinweg überleben lässt. Als zehn Plugin-Tabellen in den Framework-Namensraum umzogen, folgte ein `reference(() => table)` der Umbenennung, weil die Constraint mit der Tabelle wandert; eine handgeschriebene Text-Spalte voller Ids hätte dir nicht gesagt, dass sich etwas geändert hat.

Wenn du das App-Schema bewusst unabhängig vom Plugin-Schema willst, gibt dir `pluginRef` die Regel ohne den Schlüssel: eine schlichte typisierte Id-Spalte, keine schema-übergreifende Constraint, plus eine deklarierte Orphan-Policy, die das Framework auf dem Post-Commit-Change-Kanal fährt. Das schließt die übliche Lücke, in der eine nicht erzwungene Id-Spalte plus ein handgeschriebener Cleanup-Subscriber referenzielle Integrität pro App nachbaut — und beim ersten Vergessen still falsch ist.

Wann sollte ich KEIN Plugin schreiben?

Für einen einmaligen Seiteneffekt: ruf ihn in der Mutation auf. Für Verhalten, das nur eine Anwendung betrifft: lass es im App-Code. Für etwas Querschnittliches, das genau eine Mutation betrifft: ein einzelner Aufruf in dieser Mutation schlägt den Hook eines Plugins — ein Plugin fügt eine Indirektion hinzu, die sich erst lohnt, wenn sie breit greift.

Plugins sind für Belange, die die ganze Oberfläche schneiden: jeden Schreibvorgang auditieren, jede Mutation rate-limiten, bei jeder Registrierung ein Event senden, jede Änderung in ein Warehouse spiegeln. Wenn du die Regel nicht formulieren kannst, ohne „jede“ zu sagen, gehört sie wahrscheinlich in einen Handler.

Du kannst dir auch einen Namensraum mit einem Plugin teilen, statt es zu ersetzen. Die Kollisionsprüfung vergleicht vollständige Tags — dein `notifications.archive` steht also neben dem `notifications.inbox` des Plugins, ohne zu kollidieren. Eine der Plugin-Routen zu ersetzen ist möglich, muss aber deklariert werden: ließe man deine Route still gewinnen, könnte ein Plugin-Upgrade eine deiner Routen überschatten, ohne dass es einen Diff zu lesen gäbe.

Woran ein Plugin andocken kann.

Erweiterungspunkte eines Plugins und was sie ermöglichen
OberflächeWas sie ermöglicht
InterceptorsJede Mutation, jedes Query-Setup und jede Action umschließen — gaten, auditieren, Ein-/Ausgabe transformieren, mit getyptem Fehler scheitern.
extendSchemaTabellen und eigene SQL-Migrationen beisteuern, getrennt von der Schema-Historie der App verbucht.
servicesEin Effect-Layer bereitstellen, dessen Tags jeder Handler erreicht — so kommen MailService und StorageService zustande.
routes / httpRoutesrpc-Procedures registrieren oder rohe HTTP-Endpunkte am Framework-Listener für Flows, die keine rpc sein können.
inspectEndpointsTooling mounten, das die Dashboards lesen — ein Plugin kann so sein eigenes Betriebs-Panel mitbringen.
Lifecycle-HooksonInstall, onActivate, onDeactivate, onUninstall — plus Wrapper um Cron-Feuerungen, Workflow-Steps und Pre-Auth-Requests.

Häufige Fragen

Muss ich die mitgelieferten Plugins verwenden?

Nein. Jedes Plugin ist opt-in: du listest in der app.config.ts die, die du willst, der Rest wird nie geladen. Es sind gewöhnliche Pakete auf demselben Contract, den auch deine eigenen Plugins nutzen — ohne privilegierten Zugriff auf die Runtime.

Kann ein Plugin die Daten eines anderen Mandanten sehen?

Nein. Interceptors laufen, nachdem die Runtime das Tenant-Prädikat gemerged hat — ein Mutation-Interceptor erreicht also keine Zeilen außerhalb des Mandanten des Aufrufers, auch nicht durch Manipulation des Subjects. Die Mandantengrenze wird unterhalb der Plugin-Schicht erzwungen, nicht von ihr.

Wie greifen Plugin-Migrationen und meine ineinander?

Tabellen und Migrationen eines Plugins werden getrennt von deinem Schema verbucht — eines zu installieren verheddert seine Historie also nicht mit deiner. Seine Tabellen reiten trotzdem auf demselben deklarativen Differ, eine Schema-Änderung des Plugins wird also vom selben Codepfad angewandt wie deine, auf jedem Dialekt.

Was passiert, wenn zwei Plugins dieselbe Route registrieren?

Das wird abgelehnt statt still aufgelöst — zwei Handler hinter einem Tag ist nichts, worüber ein Aufrufer nachdenken kann. Sich einen Namensraum zu teilen ist in Ordnung, weil die Prüfung vollständige Tags vergleicht; eine Plugin-Route bewusst zu ersetzen muss deklariert werden, und der Boot protokolliert, welche Routen ersetzt wurden.

Kann ich ein Plugin nur für den internen Gebrauch meiner Firma schreiben?

Ja, und für querschnittliche interne Belange ist das der vorgesehene Weg. Der Contract ist klein genug, um ihn in einem Rutsch zu lesen, Plugins sind gewöhnliche Workspace-Pakete, und nichts an einem First-Party-Plugin ist deinem verwehrt.

Plugins komponieren mit dem Kern.

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