Security Block
The security {} block is a top-level language construct in Tova that centralizes your entire security policy in one place. It covers authentication, authorization, route protection, sensitive field handling, CORS, CSP, rate limiting, CSRF, HSTS, and audit logging. The compiler reads this block and generates all enforcement code into both server and client outputs -- with zero runtime dependencies and compile-time validation.
Why a Dedicated Security Block?
Without centralized security, configuration is scattered: auth in one file, CORS in another, rate limiting wired up per-route, CSRF tokens manually validated, field sanitization done (or forgotten) in each response handler. One missed attachment, one forgotten route, and your security has a hole.
The security {} block solves this:
- Centralized security policy -- every security concern in one block, one place to audit
- Compile-time validation -- the compiler catches undefined roles, hardcoded secrets, CORS wildcards, and missing auth before your code runs
- Zero runtime dependencies -- the compiler generates all enforcement code directly. No packages to install, update, or audit
- Cross-block enforcement -- security rules automatically apply to both server and client outputs
- Automatic coverage -- adding a new route or endpoint automatically inherits the security policy. No middleware to remember to attach
- Defense in depth -- JWT algorithm pinning, path normalization, timing-safe CSRF comparison, HSTS auto-enablement
Syntax Overview
security {
auth jwt {
secret: env("JWT_SECRET")
expires: 86400
}
role Admin {
can: [manage_users, view_analytics]
}
role User {
can: [view_profile, edit_profile]
}
protect "/api/admin/*" {
require: Admin
rate_limit: { max: 100, window: 60 }
}
protect "/api/*" {
require: authenticated
}
sensitive User.password {
hash: "bcrypt"
never_expose: true
}
sensitive User.email {
visible_to: [Admin, "self"]
}
cors {
origins: ["https://myapp.com"]
methods: [GET, POST, PUT, DELETE]
credentials: true
}
csp {
default_src: ["self"]
script_src: ["self"]
}
rate_limit {
max: 1000
window: 3600
}
csrf {
enabled: true
exempt: ["/api/webhooks/*"]
}
audit {
events: [login, logout, manage_users]
store: "audit_log"
retain: 90
}
trust_proxy true
hsts {
max_age: 63072000
include_subdomains: true
preload: true
}
}Authentication
The auth declaration configures how users authenticate. It supports JWT and API key authentication:
security {
// JWT authentication (default)
auth jwt {
secret: env("JWT_SECRET")
expires: 86400
}
}security {
// API key authentication
auth api_key {
header: "X-API-Key"
}
}When auth is configured in the security block, the server automatically generates a __authenticate() function, and the client gets getAuthToken(), setAuthToken(), and clearAuthToken() helpers with automatic token injection into RPC calls.
Token Storage
By default, auth tokens are stored in localStorage. For higher security, you can use HttpOnly cookies instead:
security {
auth jwt {
secret: env("JWT_SECRET")
expires: 86400
storage: "cookie" // HttpOnly cookie instead of localStorage
}
}With storage: "cookie":
- The server reads tokens from an
HttpOnly; Secure; SameSite=Laxcookie - The client automatically sends credentials with every RPC call
setAuthToken()is a no-op (the server sets the cookie via__setAuthCookie())clearAuthToken()calls the/rpc/__logoutendpoint to clear the cookie- Falls back to reading the
Authorization: Bearerheader if no cookie is present
Use the __setAuthCookie(response, token) helper in your login route to set the cookie:
server {
fn login(email: String, password: String) {
user = authenticate(email, password)
token = sign_jwt({ id: user.id, role: user.role })
__setAuthCookie(respond(200, { user }), token)
}
}Roles
Roles define named permission groups:
security {
role Admin {
can: [manage_users, view_analytics, delete_posts]
}
role Editor {
can: [create_posts, edit_posts, view_analytics]
}
role User {
can: [view_profile, edit_profile]
}
}On the server, this generates __hasRole(user, roleName) and __hasPermission(user, permission) functions. On the client, it generates a can(permission) helper for conditional UI rendering.
Multi-Role Users
Users can have a single role (user.role = "Admin") or multiple roles (user.roles = ["Admin", "Editor"]). The role checking functions support both formats automatically:
// Single role — works
{ id: 1, role: "Admin" }
// Multiple roles — also works
{ id: 1, roles: ["Admin", "Editor"] }On the client, setUserRole() accepts either a string or an array:
setUserRole("Admin") // single role
setUserRole(["Admin", "Editor"]) // multiple roles
can("manage_users") // checks across all rolesRoute Protection
The protect declaration secures route patterns with role requirements:
security {
// Require Admin role for admin routes
protect "/api/admin/*" {
require: Admin
rate_limit: { max: 100, window: 60 }
}
// Require any authenticated user for API routes
protect "/api/*" {
require: authenticated
}
}require: authenticated-- any authenticated user can accessrequire: RoleName-- only users with the specified role can accessrate_limit-- optional per-route rate limiting (max requests per window in seconds)
Route patterns support glob-style wildcards:
*matches within a single path segment (e.g.,/api/*/usersmatches/api/v1/usersbut not/api/v1/v2/users)**matches across path segments (e.g.,/api/**matches/api/v1/users,/api/admin/settings, etc.)- Special regex characters (
.,+,?, etc.) in patterns are escaped automatically
Protection is checked automatically on every request.
Sensitive Fields
Mark type fields as sensitive to control their visibility:
security {
// Never include password in any response
sensitive User.password {
hash: "bcrypt"
never_expose: true
}
// Only show email to admins or the user themselves
sensitive User.email {
visible_to: [Admin, "self"]
}
}This generates sanitization functions (__sanitizeUser()) that strip or filter fields based on the requesting user's role.
CORS
Configure Cross-Origin Resource Sharing:
security {
cors {
origins: ["https://myapp.com", "https://staging.myapp.com"]
methods: [GET, POST, PUT, DELETE]
credentials: true
}
}Content Security Policy
Configure CSP headers:
security {
csp {
default_src: ["self"]
script_src: ["self"]
style_src: ["self", "unsafe-inline"]
img_src: ["self", "https://cdn.example.com"]
}
}Directive names use underscores in Tova (e.g., default_src) and are converted to hyphens in the output header (e.g., default-src).
Rate Limiting
Set global rate limits:
security {
rate_limit {
max: 1000
window: 3600
}
}max-- maximum requests per windowwindow-- window duration in seconds
Per-route rate limits can also be configured inside protect declarations.
CSRF Protection
Configure CSRF token validation:
security {
csrf {
enabled: true
exempt: ["/api/webhooks/*"]
}
}Audit Logging
Track security-relevant events:
security {
audit {
events: [login, logout, manage_users]
store: "audit_log"
retain: 90
}
}events-- list of event names to trackstore-- database table name for audit entriesretain-- number of days to retain audit logs
Client-Side Helpers
When the security block is present, the client output automatically receives:
Auth Token Management
// Set token after login
setAuthToken(token)
// Get current token (auto-injected into RPC calls)
getAuthToken()
// Clear token on logout
clearAuthToken()Permission Checking
// Set the current user's role (after login)
setUserRole("Admin")
// Check if current user has a permission
if (can("manage_users")) {
// show admin panel
}Backward Compatibility
Existing inline declarations inside server {} blocks (auth {}, cors {}, rate_limit {}) continue to work. When both a security block and inline declarations exist, the inline declaration takes precedence for that specific feature.
Trust Proxy
Control how client IP addresses are determined. By default, x-forwarded-for headers are not trusted to prevent IP spoofing:
security {
// Trust proxy — read x-forwarded-for (use behind a trusted reverse proxy)
trust_proxy true
// Don't trust proxy (default) — use direct connection IP
trust_proxy false
// Only trust x-forwarded-for from loopback addresses
trust_proxy "loopback"
}This affects all IP-based features: rate limiting, route protection rate limits, and audit logging.
HSTS (HTTP Strict Transport Security)
Configure HSTS headers. When auth is configured, HSTS is automatically enabled with safe defaults:
security {
// Custom HSTS configuration
hsts {
max_age: 63072000
include_subdomains: true
preload: true
}
}security {
// Disable HSTS explicitly (even when auth is configured)
hsts {
enabled: false
}
}If no explicit hsts block is present but auth is configured, the compiler auto-generates Strict-Transport-Security: max-age=31536000; includeSubDomains.
Auto-Sanitization
When sensitive fields are declared, the compiler generates an __autoSanitize() function that automatically strips sensitive fields from all RPC responses. This means you don't need to manually call sanitization functions -- any object returned from a server function will have sensitive fields removed based on the requesting user's permissions.
The auto-sanitizer:
- Dispatches to type-specific sanitizers by checking
data.__typeordata.constructor.name - Recurses into arrays and nested objects
- Passes the authenticated user for role-based visibility checks
Compile-Time Warnings
The analyzer produces warnings for:
- Undefined roles (
W_UNDEFINED_ROLE) -- aprotectrule orsensitivevisible_toreferences a role that isn't defined in any security block - Duplicate roles (
W_DUPLICATE_ROLE) -- the same role name is defined more than once in a single security block - Protect without auth (
W_PROTECT_WITHOUT_AUTH) -- route protection rules exist but noauthconfiguration is present, meaning all protected routes will be inaccessible - Protect without require (
W_PROTECT_NO_REQUIRE) -- aprotectrule has norequirekey, leaving the route unprotected - Hardcoded secret (
W_HARDCODED_SECRET) -- the auth secret is a string literal instead ofenv("SECRET_NAME") - CORS wildcard (
W_CORS_WILDCARD) -- CORS origins contains"*", which allows any origin - Unknown auth type (
W_UNKNOWN_AUTH_TYPE) -- using an unsupported authentication type - Invalid rate limit (
W_INVALID_RATE_LIMIT) -- rate limit values are invalid - CSRF disabled (
W_CSRF_DISABLED) -- CSRF protection is explicitly disabled
Role validation works across multiple security blocks -- a role defined in one block can be referenced by a protect rule or sensitive declaration in another block.
Security Hardening
The security block implementation includes several hardening measures:
- JWT algorithm validation -- Only
HS256tokens are accepted; tokens withalg: "none"or other algorithms are rejected, preventing algorithm confusion attacks - Path normalization -- Request paths are URL-decoded, double slashes collapsed,
../sequences resolved, and trailing slashes stripped before routing, preventing path traversal bypasses - CSRF raw byte comparison -- CSRF token signatures are compared using
timingSafeEqualon raw bytes, not hex string comparison, preventing timing attacks - CSRF exempt patterns -- Webhook and API callback routes can be exempted from CSRF validation using glob patterns in
csrf { exempt: [...] } - HSTS auto-enablement -- When
authis configured, the compiler automatically generatesStrict-Transport-Security: max-age=31536000; includeSubDomainsunless explicitly disabled - Client-side advisory -- The
can()helper on the client includes a code comment noting it's for UI purposes only; all authorization is enforced server-side - Auto-sanitization -- The
__autoSanitize()function is applied to all RPC responses, recursively walking objects and arrays to strip sensitive fields based on__type/__tagmarkers
What the Compiler Generates
For a security block with auth, roles, protections, and sensitive fields, the compiler generates:
Server-side (zero runtime dependencies):
__authenticate(req)-- extracts and validates JWT/API key from request headers or cookies__hasRole(user, roleName)-- checks single-role or multi-role users__hasPermission(user, permission)-- checks if user's role(s) include the permission- Route protection middleware with glob-to-regex pattern matching
- Per-route and global rate limiting with sliding window counters
__sanitizeUser(),__sanitizeOrder(), etc. -- per-type field sanitization__autoSanitize(data, user)-- recursive response sanitization applied to all RPC returns- CSRF token generation and timing-safe validation
- Path normalization middleware
- CORS, CSP, HSTS header generation
- Audit event logging with database insertion and error handling
Client-side:
getAuthToken(),setAuthToken(token),clearAuthToken()-- token lifecycle- Automatic
Authorization: Bearerinjection into all RPC calls setUserRole(role)-- accepts string or array for multi-rolecan(permission)-- UI-side permission check (advisory only)- HttpOnly cookie mode when
storage: "cookie"is configured
All generated code has zero runtime dependencies. The security block is designed so that adding a new route or endpoint automatically inherits the security policy. You never forget to attach middleware because there is no middleware to attach -- the compiler enforces it.