Refuses to boot
rather than serve it open.
Security defaults are only real if they survive a deadline. These are enforced where forgetting is impossible: a procedure with no access decision stops the boot, a server-only column cannot reach the wire, and a secret is minted for your project rather than shipped in a template for everyone who downloads it.
// 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()What is enforced, not advised.
Default-deny, checked at boot
Every wire-reachable procedure and event must declare who may call it — a guard, or an explicit open-access reason in writing. There is no implicit "authenticated is enough". A descriptor that reaches the wire undecided is refused per request as well, behind the boot gate, because defence in depth is what catches the one that came from a third-party plugin.
Columns that cannot leave the server
Mark a column server-only and it is stripped from every wire response — not by remembering to omit it in each query, but structurally. Exposure, encryption at rest and export masking are three separate decisions with three separate markers, because conflating them is how a field ends up encrypted and still shipped to the browser.
We ship no secret values, anywhere
No template, baseline or example carries a session secret or key — not even a "clearly fake" placeholder, because a placeholder in a shipped file is a signing key published to everyone who downloads it. Secrets the framework owns are minted per project into a gitignored file, and the mint refuses to write until that file is actually ignored.
Cross-site requests are refused
State-changing endpoints are origin-checked unless they declare an exemption — and so is every WebSocket upgrade, the raw gateways included, which closes cross-site WebSocket hijacking. Sessions carry CSRF protection, the client address resolves through your declared trusted proxies rather than a header anyone can set, and a page can carry a per-request CSP nonce the framework stamps onto every script it renders — client-injected third-party tags (`<Script>`) propagate the document's nonce, with strict-dynamic as the documented answer for static pages. The defaults are the strict ones; loosening is a written decision.
Supply chain, gated per release
A dependency audit, an inbound-license allowlist that fails the publish on a bad licence, and an SBOM — run as gates rather than as a report someone reads later. A dependency that changes its licence stops a release instead of quietly changing your obligations.
Your database, your backups, your rules
Self-hosted by default: the framework is a runtime that lives next to your database. Your connection string, your backups, your access controls — nothing is proxied through our infrastructure. Data residency is declarable, so a tenant's data can be pinned to a region and requests routed there or refused. And the font pipeline self-hosts declared fonts as hashed files from your own origin — no Google-Fonts request ever leaves a visitor's browser, which is the GDPR answer German courts made concrete.
How we treat our own mistakes.
A guard whose first find is us
These defaults were not written from first principles; several were written after something got through. A session secret with a development fallback was reachable in production. An event could be subscribed by anyone while the identical shape was already refused for procedures. Each is now a gate rather than a paragraph, and the reasoning is recorded next to the code so the next person cannot relax it by accident. Security notes that say what we got wrong are more useful than ones that only say what we do right — and they are the reason these particular checks exist at all.
What a request passes through before your handler.
- 1
The credential is resolved
A session cookie, an API key or a bearer token becomes a subject — or does not, in which case the caller is anonymous rather than trusted.
- 2
Revocation and scopes are checked
A revoked session fails here, not at the first query. Scopes are resolved per connection, so a role change takes effect on the next call rather than at the next login.
- 3
The procedure's guards run
Declared on the procedure, so every entry point that reaches it goes through them — a rule written inside a handler is one a second entry point can bypass.
- 4
The tenant predicate is merged in
Only now does your code run, against a store whose reads are already bounded to the caller's tenant. Nothing you write can widen that by omission.
Security posture, in depth.
What does default-deny mean in practice?
Every procedure and event that can be reached over the wire must declare who may call it — a guard, or an explicit written reason for being open. There is no implicit "authenticated is enough", and the application refuses to boot if one is undecided, naming it.
Refusing the boot is the point. An undecided guard fails open, and a failure that only shows up as unauthorised access is one nobody notices until it matters — so it is made impossible to deploy rather than easy to detect afterwards.
It is enforced twice on purpose. The boot gate catches what is in your source; the dispatch spine refuses an undecided descriptor per request, which is what catches one that arrives from a third-party plugin or an embedder's hand-bound route. Defence in depth here is not ceremony — the second layer covers a source the first cannot see.
How are secrets handled?
None are shipped. No template, baseline or example carries a session secret or key — not even a clearly-fake placeholder, because a placeholder in a shipped file is a signing key published to everyone who downloads it, and a comment saying not to use it in production does not change that.
Secrets the framework owns are minted per project into a gitignored file during development, and the mint refuses to write until that file is actually ignored — a real secret in a committable file is worse than the placeholder it replaced. Third-party credentials are never invented: an invented API key looks right and authenticates nobody, so a missing one stays a boot failure.
Minting is development-only. In production a missing secret is a refusal to start, because the alternative — a development fallback reachable in production — is exactly the defect this framework shipped once, guarded by an environment check that a bare production boot sailed past.
What stops sensitive data reaching the client?
A server-only marker on the column, which strips it from every wire response structurally rather than by remembering to omit it per query. The boundary is below the query layer, so a new endpoint cannot accidentally include it.
Exposure, encryption at rest and export masking are three separate markers on purpose. Conflating them is how a field ends up encrypted in the database and still sent to the browser — each answers a different question, and treating one as evidence of another is the mistake.
Cross-site requests are refused by default: state-changing endpoints are origin-checked unless they declare an exemption, sessions carry CSRF protection, and the client address is resolved through your declared trusted proxies rather than a header any caller can set.
How is the supply chain handled?
As gates rather than reports. A dependency audit, an inbound-licence allowlist that fails the publish on a licence we cannot accept, and an SBOM — run as part of releasing rather than as a document somebody reads later. A dependency that changes its licence stops a release instead of quietly changing your obligations.
The same posture applies to our own surface: a public API cannot drift unnoticed because every published entry point carries a golden that CI compares. That gate had three packages missing from it, which is exactly how a documentation page came to promise a hook that did not exist.
Self-hosting is the other half. The framework is a runtime beside your database — your connection string, your backups, your access rules — and nothing is proxied through our infrastructure. Data residency is declarable, so a tenant's data can be pinned to a region and requests routed there or refused. And the font pipeline self-hosts declared fonts as hashed files from your own origin — no Google-Fonts request ever leaves a visitor's browser, which is the GDPR answer German courts made concrete.
What is enforced, and where.
| Control | Where it is enforced |
|---|---|
| Access decisions | Boot gate over your source, plus per-request refusal in the dispatch spine for descriptors it cannot see. |
| Tenant boundary | A predicate merged by the runtime beneath handlers and plugins; fails closed when no tenant resolves. |
| Wire exposure | Server-only columns stripped structurally — separate from encryption at rest and export masking. |
| Secrets | None shipped; framework-owned ones minted per project into a gitignored file, dev-only. |
| Cross-site | Origin checks on state-changing routes AND every WebSocket upgrade, CSRF on sessions, client IP via declared trusted proxies, per-request CSP nonces stamped through the render. |
| Supply chain | Dependency audit, inbound-licence allowlist and SBOM as release gates rather than reports. |
Frequently asked questions
What happens if I forget to declare access on a procedure?
The application refuses to boot and names it. That is deliberate: an undecided guard fails open, and the resulting bug is invisible until someone exploits it — so it is made undeployable rather than merely detectable.
Are there any default credentials or example secrets?
None, anywhere. A placeholder in a shipped file is a signing key published to every downloader, and no comment changes that. Framework-owned secrets are minted per project into a gitignored file during development, and the mint refuses to write until the file is genuinely ignored.
Can I keep data in a specific region?
Yes — declare the regions you serve and the framework opens a store per region, routing each request to its tenant's home region or refusing it. The guarantee is enforced at boot rather than promised in a document.
How do I report a vulnerability?
Through the disclosure process on the security documentation page, which also lists supported versions and the supply-chain gates each release passes. Please report privately rather than opening a public issue.
Does my data pass through your infrastructure?
No. Self-hosting is the default posture: the framework runs beside your database with your connection string, your backups and your access rules. Nothing is proxied to third-party infrastructure unless you explicitly choose a managed target.
Where security shows up.
Open the framework. See it for yourself.
Every primitive on this page is in the framework today. Clone the starter, run `voltro dev`, and have it on screen in two minutes.