Skip to content
Get Started

Security & Authentication

Zebric includes built-in security features for authentication, authorization, and request protection.

Enable email/password authentication:

[auth]
providers = ["email"]
trustedOrigins = ["http://localhost:3000"]

Authentication is powered by Better Auth with server-side session management.

Control authentication requirements per page:

[page."/dashboard"]
title = "Dashboard"
auth = "required" # Must be logged in
layout = "dashboard"
[page."/posts"]
title = "Public Posts"
auth = "none" # No login needed
layout = "list"
[page."/profile"]
title = "My Profile"
auth = "optional" # Shows user info if logged in
layout = "detail"

The auth layout renders sign-in and sign-up forms:

[page."/sign-in"]
title = "Sign In"
layout = "auth"

Define CRUD permissions on entities:

[entity.Post.access]
read = true # Anyone can read
create = true # Anyone can create
update = { authorId = "$currentUser.id" } # Only author can update
delete = { "$currentUser.role" = "admin" } # Only admins can delete
PatternDescription
{ 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
# Authors can see their own drafts; everyone sees published posts
[entity.Post.access]
read = { or = [{ status = "published" }, { authorId = "$currentUser.id" }] }

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 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 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"] }
FieldDescription
nameConfiguration name and fallback identity for fields not supplied explicitly.
keyEnvEnvironment variable holding the secret. The secret is never stored in the Blueprint.
agentIdStable actor identity used by policy and audit attribution. Defaults to name.
credentialIdIdentity of this particular credential, used for ownership and idempotency isolation. Defaults to name.
displayNameHuman-readable session name. Defaults to name.
rolesApplication roles available through actor.roles and RBAC.
scopesTransport permissions required by entity, skill, workflow, or command routes.
constraintsApplication-defined string-list constraints preserved on the actor.

Set the env var before starting the server:

Terminal window
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:

Terminal window
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:

OperationRequired scope
Listentity.<entity>.list
Get oneentity.<entity>.get
Createentity.<entity>.create
Updateentity.<entity>.update
Deleteentity.<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.

  1. At startup, the runtime reads auth.apiKeys, resolves each keyEnv from process.env, and stores a verifier rather than the plaintext key.
  2. Supported API routes check the bearer token before falling back to session authentication.
  3. A valid key creates a synthetic agent session. user.id is the agent ID, while actor.credentialId preserves 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.

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
[auth.session]
duration = 86400 # Session duration in seconds (default: 24 hours)
idle_timeout = 3600 # Idle timeout in seconds