Verweigert den Boot,
statt es offen auszuliefern.

Sicherheits-Defaults sind nur echt, wenn sie eine Deadline überleben. Diese werden dort durchgesetzt, wo Vergessen unmöglich ist: eine Procedure ohne Zugriffsentscheidung stoppt den Boot, eine server-only-Spalte kommt nicht auf die Leitung, und ein Secret wird für dein Projekt gemintet statt in einem Template an jeden ausgeliefert, der es herunterlädt.

invoices.query.ts
TypeScript
// No access decision = the boot REFUSES. Not a warning.
export const listInvoices = defineQuery({
  name: 'invoices.list',
  guards: [{ scope: 'billing:manage' }],   // ← or openAccess: '<reason>'
})

// This column cannot reach the wire, in any query, by construction
hashedKey: text().serverOnly()

Was durchgesetzt wird, nicht empfohlen.

Default-Deny, beim Boot geprüft

Jede über die Leitung erreichbare Procedure und jedes Event muss deklarieren, wer sie aufrufen darf — ein Guard oder ein ausgeschriebener Grund für offenen Zugriff. Es gibt kein implizites „authentifiziert reicht". Ein Descriptor, der unentschieden die Leitung erreicht, wird zusätzlich pro Request abgelehnt, hinter dem Boot-Gate — denn Tiefenverteidigung ist das, was den erwischt, der aus einem Fremd-Plugin kam.

Spalten, die den Server nicht verlassen können

Markier eine Spalte als server-only, und sie wird aus jeder Wire-Antwort entfernt — nicht dadurch, dass man sie in jeder Query wegzulassen erinnert, sondern strukturell. Exposition, Verschlüsselung at rest und Export-Maskierung sind drei getrennte Entscheidungen mit drei getrennten Markern, denn sie zu vermengen ist der Weg, auf dem ein Feld verschlüsselt und trotzdem an den Browser ausgeliefert wird.

Wir liefern nirgends Secret-Werte aus

Kein Template, keine Baseline, kein Beispiel trägt ein Session-Secret oder einen Key — auch keinen „offensichtlich falschen" Platzhalter, denn ein Platzhalter in einer ausgelieferten Datei ist ein Signierschlüssel, der an alle veröffentlicht wurde, die sie herunterladen. Secrets, die dem Framework gehören, werden pro Projekt in eine gitignorierte Datei gemintet — und das Minten verweigert, solange diese Datei nicht wirklich ignoriert ist.

Cross-Site-Requests werden abgelehnt

Zustandsändernde Endpunkte werden origin-geprüft, sofern sie keine Ausnahme deklarieren — und ebenso jeder WebSocket-Upgrade, die Raw-Gateways eingeschlossen, was Cross-Site-WebSocket-Hijacking schließt. Sessions tragen CSRF-Schutz, die Client-Adresse wird über deine deklarierten Trusted Proxies aufgelöst statt über einen Header, den jeder setzen kann, und eine Seite kann eine per-Request-CSP-Nonce tragen, die das Framework auf jedes gerenderte Script stempelt — client-injizierte Third-Party-Tags (`<Script>`) propagieren die Dokument-Nonce, mit strict-dynamic als dokumentierter Antwort für statische Seiten. Die Defaults sind die strengen; lockern ist eine schriftliche Entscheidung.

Lieferkette, pro Release gegated

Ein Dependency-Audit, eine Inbound-Lizenz-Allowlist, die den Publish bei einer schlechten Lizenz scheitern lässt, und eine SBOM — als Gates, nicht als Bericht, den später jemand liest. Eine Dependency, die ihre Lizenz ändert, stoppt ein Release, statt still deine Pflichten zu ändern.

Deine Datenbank, deine Backups, deine Regeln

Self-hosted per Default: das Framework ist eine Runtime, die neben deiner Datenbank lebt. Dein Connection-String, deine Backups, deine Zugriffsregeln — nichts wird durch unsere Infrastruktur geleitet. Datenresidenz ist deklarierbar, die Daten eines Mandanten lassen sich also auf eine Region festnageln und Requests dorthin routen oder ablehnen. Und die Font-Pipeline self-hostet deklarierte Fonts als gehashte Dateien vom eigenen Origin — kein Google-Fonts-Request verlässt je den Browser eines Besuchers, die DSGVO-Antwort, die deutsche Gerichte konkret gemacht haben.

Wie wir mit eigenen Fehlern umgehen.

Ein Guard, dessen erster Fund wir sind

Diese Defaults sind nicht aus ersten Prinzipien entstanden; mehrere wurden geschrieben, nachdem etwas durchgerutscht war. Ein Session-Secret mit Entwicklungs-Fallback war in Produktion erreichbar. Ein Event konnte von jedem abonniert werden, während dieselbe Form für Procedures längst abgelehnt wurde. Jedes davon ist heute ein Gate statt eines Absatzes, und die Begründung steht neben dem Code, damit die nächste Person es nicht versehentlich lockert. Sicherheitsnotizen, die sagen, was wir falsch gemacht haben, sind nützlicher als solche, die nur sagen, was wir richtig machen — und sie sind der Grund, warum es diese Checks überhaupt gibt.

Was ein Request vor deinem Handler durchläuft.

  1. 1

    Die Credential wird aufgelöst

    Ein Session-Cookie, ein API-Key oder ein Bearer-Token wird zu einem Subjekt — oder eben nicht, dann ist der Aufrufer anonym statt vertrauenswürdig.

  2. 2

    Widerruf und Scopes werden geprüft

    Eine widerrufene Session scheitert hier, nicht an der ersten Query. Scopes werden pro Verbindung aufgelöst, eine Rollenänderung wirkt also beim nächsten Aufruf statt beim nächsten Login.

  3. 3

    Die Guards der Prozedur laufen

    Sie stehen an der Prozedur, jeder Einstiegspunkt geht also durch sie hindurch — eine Regel im Handler ist eine, die ein zweiter Einstiegspunkt umgehen kann.

  4. 4

    Das Mandantenprädikat wird eingemerged

    Erst jetzt läuft dein Code, gegen einen Store, dessen Reads bereits auf den Mandanten des Aufrufers begrenzt sind. Nichts, was du schreibst, kann das durch Auslassen aufweiten.

Sicherheitshaltung im Detail.

Was heißt Default-Deny in der Praxis?

Jede Procedure und jedes Event, das über die Leitung erreichbar ist, muss deklarieren, wer sie aufrufen darf — ein Guard oder ein ausgeschriebener Grund, offen zu sein. Es gibt kein implizites „authentifiziert reicht“, und die Anwendung verweigert den Boot, wenn eines unentschieden ist, und benennt es.

Den Boot zu verweigern ist der Punkt. Ein unentschiedener Guard scheitert offen, und ein Fehler, der sich nur als unbefugter Zugriff zeigt, fällt niemandem auf, bevor es zählt — er wird also undeploybar gemacht statt hinterher leicht erkennbar.

Es wird bewusst zweimal durchgesetzt. Das Boot-Gate erwischt, was in deinem Quellcode steht; das Dispatch-Rückgrat lehnt einen unentschiedenen Descriptor pro Request ab — und das erwischt einen, der aus einem Fremd-Plugin oder einer handgebundenen Route eines Einbetters kommt. Tiefenverteidigung ist hier keine Zeremonie: die zweite Schicht deckt eine Quelle ab, die die erste nicht sehen kann.

Wie wird mit Secrets umgegangen?

Es wird keines ausgeliefert. Kein Template, keine Baseline, kein Beispiel trägt ein Session-Secret oder einen Schlüssel — nicht einmal einen offensichtlich falschen Platzhalter. Denn ein Platzhalter in einer ausgelieferten Datei ist ein Signierschlüssel, veröffentlicht an alle, die sie herunterladen, und ein Kommentar „nicht in Produktion verwenden“ ändert daran nichts.

Secrets, die dem Framework gehören, werden in der Entwicklung pro Projekt in eine gitignorierte Datei gemintet — und das Minten verweigert, solange diese Datei nicht wirklich ignoriert ist: ein echtes Secret in einer committbaren Datei ist schlimmer als der ersetzte Platzhalter. Fremd-Credentials werden nie erfunden: ein erfundener API-Key sieht richtig aus und authentifiziert niemanden, ein fehlender bleibt also ein Boot-Fehler.

Minten ist reine Entwicklung. In Produktion ist ein fehlendes Secret eine Startverweigerung — denn die Alternative, ein in Produktion erreichbarer Entwicklungs-Fallback, ist genau der Defekt, den dieses Framework einmal ausgeliefert hat, abgesichert durch eine Umgebungsprüfung, an der ein nackter Produktions-Boot vorbeisegelte.

Was hindert sensible Daten daran, den Client zu erreichen?

Ein Server-only-Marker an der Spalte, der sie strukturell aus jeder Wire-Antwort entfernt, statt dass man pro Query daran denkt, sie wegzulassen. Die Grenze liegt unter der Query-Schicht — ein neuer Endpunkt kann sie also nicht versehentlich mitliefern.

Exposition, Verschlüsselung at rest und Export-Maskierung sind bewusst drei getrennte Marker. Sie zu vermengen ist der Weg, auf dem ein Feld in der Datenbank verschlüsselt und trotzdem an den Browser gesendet wird — jeder beantwortet eine andere Frage, und einen als Beleg für einen anderen zu nehmen ist der Fehler.

Cross-Site-Requests werden standardmäßig abgelehnt: zustandsändernde Endpunkte werden origin-geprüft, sofern sie keine Ausnahme deklarieren, Sessions tragen CSRF-Schutz, und die Client-Adresse wird über deine deklarierten Trusted Proxies aufgelöst statt über einen Header, den jeder Aufrufer setzen kann.

Wie wird die Lieferkette behandelt?

Als Gates statt als Berichte. Ein Dependency-Audit, eine Inbound-Lizenz-Allowlist, die den Publish bei einer nicht akzeptablen Lizenz scheitern lässt, und eine SBOM — als Teil des Releases statt als Dokument, das später jemand liest. Eine Dependency, die ihre Lizenz ändert, stoppt ein Release, statt still deine Pflichten zu ändern.

Dieselbe Haltung gilt für unsere eigene Oberfläche: eine öffentliche API kann nicht unbemerkt driften, weil jeder veröffentlichte Entry-Point ein Golden trägt, das CI vergleicht. In genau diesem Gate fehlten drei Pakete — und genau so kam eine Doku-Seite dazu, einen Hook zu versprechen, den es nicht gab.

Self-Hosting ist die andere Hälfte. Das Framework ist eine Runtime neben deiner Datenbank — dein Connection-String, deine Backups, deine Zugriffsregeln — und nichts wird durch unsere Infrastruktur geleitet. Datenresidenz ist deklarierbar, die Daten eines Mandanten lassen sich also auf eine Region festnageln und Requests dorthin routen oder ablehnen. Und die Font-Pipeline self-hostet deklarierte Fonts als gehashte Dateien vom eigenen Origin — kein Google-Fonts-Request verlässt je den Browser eines Besuchers, die DSGVO-Antwort, die deutsche Gerichte konkret gemacht haben.

Was durchgesetzt wird — und wo.

Sicherheitskontrollen und wo jede durchgesetzt wird
KontrolleWo sie durchgesetzt wird
ZugriffsentscheidungenBoot-Gate über deinem Quellcode plus Ablehnung pro Request im Dispatch für Descriptoren, die es nicht sieht.
MandantengrenzeEin von der Runtime unter Handlern und Plugins gemergtes Prädikat; scheitert geschlossen ohne Mandanten.
Wire-ExpositionServer-only-Spalten strukturell entfernt — getrennt von Verschlüsselung at rest und Export-Maskierung.
SecretsKeine ausgeliefert; framework-eigene pro Projekt in eine gitignorierte Datei gemintet, nur in der Entwicklung.
Cross-SiteOrigin-Prüfung auf zustandsändernden Routen UND jedem WebSocket-Upgrade, CSRF auf Sessions, Client-IP über deklarierte Trusted Proxies, per-Request-CSP-Nonces durch den Render gestempelt.
LieferketteDependency-Audit, Inbound-Lizenz-Allowlist und SBOM als Release-Gates statt als Berichte.

Häufige Fragen

Was passiert, wenn ich die Zugriffsdeklaration an einer Procedure vergesse?

Die Anwendung verweigert den Boot und benennt sie. Das ist Absicht: ein unentschiedener Guard scheitert offen, und der entstehende Fehler ist unsichtbar, bis ihn jemand ausnutzt — er wird also nicht deploybar gemacht statt nur erkennbar.

Gibt es Standard-Credentials oder Beispiel-Secrets?

Keine, nirgends. Ein Platzhalter in einer ausgelieferten Datei ist ein Signierschlüssel, veröffentlicht an jeden, der sie herunterlädt, und kein Kommentar ändert das. Framework-eigene Secrets werden in der Entwicklung pro Projekt in eine gitignorierte Datei gemintet, und das Minten verweigert, solange die Datei nicht wirklich ignoriert ist.

Kann ich Daten in einer bestimmten Region halten?

Ja — deklarier die Regionen, die du bedienst, und das Framework öffnet einen Store pro Region und routet jeden Request in die Heimatregion seines Mandanten oder lehnt ihn ab. Die Zusicherung wird beim Boot durchgesetzt statt in einem Dokument versprochen.

Wie melde ich eine Schwachstelle?

Über den Offenlegungsprozess auf der Sicherheits-Doku-Seite, die auch unterstützte Versionen und die Lieferketten-Gates jedes Releases nennt. Bitte vertraulich melden statt ein öffentliches Issue zu öffnen.

Laufen meine Daten durch eure Infrastruktur?

Nein. Self-Hosting ist die Standardhaltung: das Framework läuft neben deiner Datenbank, mit deinem Connection-String, deinen Backups und deinen Zugriffsregeln. Nichts wird an fremde Infrastruktur weitergereicht, außer du wählst ausdrücklich ein Managed-Ziel.

Wo Sicherheit auftaucht.

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