Skip to content
Get Started

Runtime & Deployment

The primary way to run Zebric in development:

Terminal window
zebric dev --blueprint blueprint.toml --port 3000 --seed

This starts a Hono HTTP server with:

  • Hot reload via WebSocket (blueprint changes apply instantly)
  • SQLite database (auto-created from entity definitions)
  • Server-side HTML rendering with Tailwind CSS
  • CSRF protection, security headers, and session management

When using the engine programmatically:

import { ZebricEngine } from '@zebric/runtime-node'
const engine = new ZebricEngine({
blueprintPath: './blueprint.toml',
port: 3000,
host: 'localhost',
database: {
url: 'sqlite://./data.db',
},
dev: {
hotReload: true,
seed: true,
logLevel: 'info', // debug | info | warn | error
logQueries: false,
},
})
await engine.start()

SQLite (default):

database: {
url: 'sqlite://./data.db'
}

PostgreSQL:

database: {
url: 'postgres://user:pass@localhost:5432/mydb'
}

The current engine selects the adapter from the database.url scheme. Use sqlite://<path>, postgres://..., or postgresql://.... In development, dev.dbPath can set the SQLite path when database.url is omitted. Other fields on the DatabaseConfig type are not currently consumed by engine initialization.

See the Cloudflare deployment guide for required setup, supported features, the maintained starter/blog, and adapter and operational limitations.

Deploy to Cloudflare Workers using @zebric/runtime-worker — same blueprint format, running on Cloudflare’s edge network. The fastest way to start is copying starters/cloudflare-workers, which is a single-file entry point that fails fast when auth is not explicitly configured:

worker.ts
import { createWorkerHandler } from '@zebric/runtime-worker'
import blueprintToml from './blueprint.toml'
export default createWorkerHandler(env => {
if (!env.BETTER_AUTH_URL || !env.BETTER_AUTH_SECRET || env.BETTER_AUTH_SECRET.length < 32) {
throw new Error('Set BETTER_AUTH_URL and a BETTER_AUTH_SECRET of at least 32 characters')
}
const origin = new URL(env.BETTER_AUTH_URL)
const local = ['localhost', '127.0.0.1', '[::1]'].includes(origin.hostname)
if (origin.protocol !== 'https:' && !(local && origin.protocol === 'http:')) {
throw new Error('BETTER_AUTH_URL must use HTTPS outside local development')
}
return { blueprintContent: blueprintToml, blueprintFormat: 'toml' }
})

Without these checks the engine falls back to a localhost auth URL and a development secret, which is unsafe in a deployed Worker. Blueprints without [auth] can pass a plain config object instead.

The handler retains one engine per isolate and initializes bundled plugins once. Command receipts and event outbox entries persist in D1. Bind WORKFLOWS for durable workflow jobs and submission replay across isolates.

WorkersEnv requires DB (D1). When the Blueprint declares [auth], the Worker runtime uses Better Auth on that D1 binding and mounts the same /api/auth/* and /auth/* paths as Node. Set BETTER_AUTH_URL to the public origin, store BETTER_AUTH_SECRET with wrangler secret put, include Better Auth’s generated tables in the D1 migrations, and enable nodejs_compat.

File-backed Blueprint templates can be passed through the engine’s templates map after importing them as Wrangler text modules. TEMPLATES_KV is an alternative: store each template under template:<source> and the engine will preload referenced templates before rendering. SESSION_KV remains available for custom KV-backed session integrations. CACHE_KV creates the adapter returned by engine.getCache(), but request execution does not use it automatically. FILES creates the R2 adapter returned by engine.getStorage() and serves stored objects at /uploads/<key>, matching Node’s public file-serving route. The reserved _zebric/ object prefix is private and cannot be served there. These routes serve existing files; neither runtime provides a generic upload API. The engine applies double-submit CSRF protection when auth or sessions are configured. The separate KV session-backed CSRF adapter remains available for custom composition.

Workers also expose Node-compatible /api/<entities> CRUD routes. Blueprint API keys are loaded from Worker secret bindings named by keyEnv, with roles, entity scopes, constraints, and X-Agent-Run-ID attribution enforced. Clients can inspect the supported Worker surface at /.well-known/zebric-agent.json and /api/openapi.json.

Workers execute declarative commands through the shared core command pipeline. Bundle custom command handlers in commandHandlers, keyed by their Blueprint handler references. General workflows support intermediate query results, typed assignments, nested conditions and loops, services, notifications, external effects, and delays. Database-only transactional workflows support reads and declarative commands with atomic D1 commits and rollback; custom handlers and external effects are rejected inside transactional workflows on both runtimes.

With a WORKFLOWS binding, workflow jobs use Cloudflare’s durable checkpoints, delays, retries, and owner-authorized polling, cancellation, and retry routes. Without it, workflows run inline and their job metadata and submission replay remain isolate-local; command receipts remain durable. Configure native workflows:

import { createWorkerHandler } from '@zebric/runtime-worker'
import { createWorkflowEntrypoint } from '@zebric/runtime-worker/workflows'
import blueprintToml from './blueprint.toml'
const config = { blueprintContent: blueprintToml, blueprintFormat: 'toml' as const }
export class ZebricWorkflow extends createWorkflowEntrypoint(config) {}
export default createWorkerHandler(config)
[[workflows]]
name = "zebric-workflows"
binding = "WORKFLOWS"
class_name = "ZebricWorkflow"

Workers initialize Blueprint notifications automatically. Console and Slack use portable adapters; Slack secrets come from Worker bindings and inbound requests at /notifications/<adapter>/inbound require valid Slack signatures. Email writes to a private R2 outbox under _zebric/email-outbox/; as with Node’s file outbox, this development adapter does not deliver mail. Supply notificationFactories for a delivery provider or another adapter type.

Statically import plugins into the handler’s plugins map, keyed by Blueprint plugin name. Enabled modules initialize once before fetch, cron, or native workflow execution. Their workflow actions and named service integrations are available to the executor. All bundled plugins are trusted application code; Worker bundling does not create a sandbox for a plugin declaring limited trust.

Command, CRUD mutation, and workflow history is persisted in D1. Workers also record auth outcomes, CSRF violations, denied requests, and successful page/API reads. /metrics uses the same Prometheus counters and histograms as Node, with isolate-local HTTP/query totals. See runtime parity and platform differences for the tested contract and deployment constraints.

wrangler.toml
name = "my-zebric-app"
main = "worker.ts"
compatibility_date = "2026-04-28"
compatibility_flags = ["nodejs_compat"]
[[d1_databases]]
binding = "DB"
database_name = "my-zebric-db"
database_id = "your-database-id" # from: wrangler d1 create my-zebric-db
# Optional
[[kv_namespaces]]
binding = "SESSION_KV"
id = "your-session-kv-id" # from: wrangler kv namespace create SESSION_KV

Create the D1 database, apply your migrations, then run locally or deploy:

Terminal window
wrangler d1 create my-zebric-db # copy the database_id into wrangler.toml
wrangler dev # local dev at http://localhost:8787, with D1/KV emulation
wrangler deploy # deploy to your-app.your-subdomain.workers.dev

For a working reference, see starters/cloudflare-workers.

FROM node:24-slim
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --prod
COPY blueprint.toml ./
EXPOSE 3000
CMD ["npx", "zebric", "dev", "--blueprint", "blueprint.toml"]
Terminal window
wrangler deploy

The filesystem, admin-port and local monitoring examples below apply to Node. Workers uses D1 audit history, isolate-local metrics and Cloudflare observability; see the Workers guide.

Security events are written as JSON to ./data/audit.log (configurable). Events include authentication attempts, access control denials, CSRF violations, data access, and suspicious activity. Each entry includes timestamp, event type, severity, user context, and sanitized metadata.

Sensitive fields (passwords, tokens, API keys) are automatically redacted.

The engine exposes Prometheus-format metrics with zbl_* prefixed counters and histograms, registered on both the main app port and the admin port:

GET http://localhost:3000/metrics # main app port
GET http://localhost:3030/metrics # admin port (127.0.0.1 only)

Available metrics:

  • zbl_requests_total — total HTTP requests
  • zbl_request_duration_ms — request duration histogram (buckets: 25, 50, 100, 250, 500, 1000, 2000ms)
  • zbl_requests_by_route_total — requests per route
  • zbl_requests_by_status_total — requests by status code family (2xx, 4xx, etc.)
  • zbl_query_duration_ms — database query duration histogram per entity
  • zbl_route_cache_hits / zbl_route_cache_misses — route cache performance
GET http://localhost:3000/health # main app port
GET http://localhost:3030/health # admin port

Returns health status with a 200 (healthy) or 503 (unhealthy) status code.

docker-compose.dev.yml includes Prometheus and Grafana services pre-configured to scrape your local Zebric instance.

Terminal window
# Prometheus only
docker compose -f docker-compose.dev.yml up zebric-dev prometheus-dev
# Prometheus + Grafana
docker compose -f docker-compose.dev.yml up zebric-dev prometheus-dev grafana-dev
  • Prometheus UI: http://localhost:9091 — check Status → Targets to confirm zebric-engine shows UP, or query directly at /graph?g0.expr=zbl_requests_total
  • Grafana: http://localhost:3001 (default credentials admin / admin). It has no Prometheus data source pre-configured — add one manually under Connections → Data sources, type Prometheus, URL http://prometheus-dev:9090 (the Docker service name, not localhost)

The scrape config lives at monitoring/prometheus-dev.yml (10s interval, targets zebric-dev:3000). Update the targets field there if you change the app port or container name.

Prometheus data persists in the prometheus_dev_data volume between runs; docker compose -f docker-compose.dev.yml down -v resets it.

The admin server runs on a separate port (default: 3030, bound to 127.0.0.1 only) and exposes:

EndpointDescription
/healthEngine health status
/metricsPrometheus-format metrics
/blueprintCurrent parsed blueprint
/stateEngine state
/entitiesEntity summary
/pagesPage routes and layouts
/workflowsWorkflow definitions, job counts, and stats
/workflows/visualizationHTML workflow graph visualization
/tracesRequest traces (with ?limit=N)
/traces/errorsError traces
/traces/slowSlow traces (with ?threshold=1000)
/traces/statsTrace statistics
/schema-diffPending database schema changes
/pluginsLoaded plugin list