Security & Authentication
Zebric includes built-in security features for authentication, authorization, and request protection.
Authentication
Section titled “Authentication”Enable email/password authentication:
[auth]providers = ["email"]trustedOrigins = ["http://localhost:3000"]Authentication is powered by Better Auth with server-side session management.
Page-Level Auth
Section titled “Page-Level Auth”Control authentication requirements per page:
[page."/dashboard"]title = "Dashboard"auth = "required" # Must be logged inlayout = "dashboard"
[page."/posts"]title = "Public Posts"auth = "none" # No login neededlayout = "list"
[page."/profile"]title = "My Profile"auth = "optional" # Shows user info if logged inlayout = "detail"Auth Pages
Section titled “Auth Pages”The auth layout renders sign-in and sign-up forms:
[page."/sign-in"]title = "Sign In"layout = "auth"Access Control
Section titled “Access Control”Entity-Level Access
Section titled “Entity-Level Access”Define CRUD permissions on entities:
[entity.Post.access]read = true # Anyone can readcreate = true # Anyone can createupdate = { authorId = "$currentUser.id" } # Only author can updatedelete = { "$currentUser.role" = "admin" } # Only admins can deleteCondition Operators
Section titled “Condition Operators”| Pattern | Description |
|---|---|
{ field = "value" } | Field equals value |
{ field = "$currentUser.id" } | Field matches current user |
{ "$currentUser.role" = "admin" } | Current user has role |
{ or = [...] } | Any condition matches |
{ and = [...] } | All conditions match |
Combined Conditions
Section titled “Combined Conditions”# Authors can see their own drafts; everyone sees published posts[entity.Post.access]read = { or = [{ status = "published" }, { authorId = "$currentUser.id" }] }Field-Level Access
Section titled “Field-Level Access”Restrict read/write on individual fields:
[entity.User]fields = [ { name = "email", type = "Email", access = { read = true, write = { "$currentUser.role" = "admin" } } }, { name = "salary", type = "Integer", access = { read = { "$currentUser.role" = "admin" }, write = false } }]Role-Based Permissions
Section titled “Role-Based Permissions”Role policies add a second authorization layer across entities:
[auth]providers = ["email"]
[auth.permissions]member = { allow = ["Post.read", "Post.update"] }suspended = { allow = [], deny = ["*.*"] }admin = { allow = ["*.*"] }anonymous = { allow = ["Post.read"] }Permission patterns use exactly Entity.action. Actions are read, create,
update, delete, or *. An explicit deny in any role overrides allows from
all of the user’s other roles. Conditional permissions evaluate record fields
and $currentUser.* values only when those values exist; empty or incomplete
conditions deny access.
When RBAC is configured, manual workflow requests must identify a blueprint page that exposes that workflow. Zebric checks the caller against every entity action the workflow can perform before starting it. Workflows with no query steps use the page’s primary entity and require update permission.
API Keys
Section titled “API Keys”API keys authenticate agents, REST clients, and external integrations. They apply to entity CRUD, skill actions, workflow actions and jobs, domain commands, and authorized audit-history queries. Browser page routes continue to use sessions, and /api/auth/* remains part of the interactive authentication flow.
[[auth.apiKeys]]name = "dispatch-agent"keyEnv = "DISPATCH_AGENT_API_KEY"agentId = "dispatch-agent"credentialId = "dispatch-production"displayName = "Dispatch Agent"roles = ["operator"]scopes = ["entity.issue.list", "entity.issue.get", "issues.transition"]constraints = { environment = ["production"] }| Field | Description |
|---|---|
name | Configuration name and fallback identity for fields not supplied explicitly. |
keyEnv | Environment variable holding the secret. The secret is never stored in the Blueprint. |
agentId | Stable actor identity used by policy and audit attribution. Defaults to name. |
credentialId | Identity of this particular credential, used for ownership and idempotency isolation. Defaults to name. |
displayName | Human-readable session name. Defaults to name. |
roles | Application roles available through actor.roles and RBAC. |
scopes | Transport permissions required by entity, skill, workflow, or command routes. |
constraints | Application-defined string-list constraints preserved on the actor. |
Set the env var before starting the server:
export DISPATCH_AGENT_API_KEY="sk-your-secret-key-here"If the env var isn’t set at startup, the key is skipped and a warning is logged — the server still starts normally.
After a bearer credential is validated, its API requests skip browser CSRF validation but still go through scope checks, access control, command policy, and state validation. Send the key in the Authorization header. Mutating entity, skill, and workflow requests from an agent also require a safe X-Agent-Run-ID containing 1–128 letters, digits, dots, underscores, colons, or hyphens:
curl -X POST http://localhost:3000/api/issues/01J.../status \ -H "Authorization: Bearer sk-your-secret-key-here" \ -H "X-Agent-Run-ID: dispatch-2026-09-30T1600" \ -H "Content-Type: application/json" \ -d '{"status": "in_progress"}'Generic entity endpoints use these scopes, where the entity name is lowercase:
| Operation | Required scope |
|---|---|
| List | entity.<entity>.list |
| Get one | entity.<entity>.get |
| Create | entity.<entity>.create |
| Update | entity.<entity>.update |
| Delete | entity.<entity>.delete |
An agent credential with scopes = ["*"] passes every transport scope check. Prefer the narrowest explicit scopes suitable for the integration. Domain commands and skill actions use their declared scopes arrays.
How It Works
Section titled “How It Works”- At startup, the runtime reads
auth.apiKeys, resolves eachkeyEnvfromprocess.env, and stores a verifier rather than the plaintext key. - Supported API routes check the bearer token before falling back to session authentication.
- A valid key creates a synthetic agent session.
user.idis the agent ID, whileactor.credentialIdpreserves the identity of the credential that made the request. The session flows through workflows, commands, audit events, and access control:
{ "id": "apikey-dispatch-production", "userId": "dispatch-agent", "user": { "id": "dispatch-agent", "name": "Dispatch Agent", "email": "", "roles": ["operator"] }, "actor": { "type": "agent", "id": "dispatch-agent", "credentialId": "dispatch-production", "roles": ["operator"], "scopes": ["entity.issue.list", "entity.issue.get", "issues.transition"] }}{{ variables.data.user.id }} resolves to the agent ID. Audit and job records can distinguish the agent, credential, and X-Agent-Run-ID invocation.
The configured secret remains sensitive even though the runtime stores a verifier in memory. Rotate it by changing the environment variable and restarting the server.
Built-In Protections
Section titled “Built-In Protections”Zebric applies these security measures automatically:
- CSRF protection on all state-changing requests (POST, PUT, DELETE)
- Security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy)
- Open redirect prevention on login redirect flows
- Path traversal protection on file resolution
- HTML entity encoding to prevent XSS
- Workflow body filtering to prevent injection in workflow payloads
- Audit logging for security-relevant events
- Error sanitization to avoid leaking internal details
Session Configuration
Section titled “Session Configuration”[auth.session]duration = 86400 # Session duration in seconds (default: 24 hours)idle_timeout = 3600 # Idle timeout in seconds