Skip to content
Get Started

Blueprint Reference

Blueprints are TOML files that declaratively define your application. This page covers every section of a blueprint.

Every blueprint starts with a version and project metadata:

version = "0.1.0"
[project]
name = "My App"
version = "1.0.0"
description = "An internal tool"
[project.runtime]
min_version = "0.1.0"

Entities define your data model. The runtime creates database tables automatically.

[entity.Post]
fields = [
{ name = "id", type = "ULID", primary_key = true },
{ name = "title", type = "Text", required = true },
{ name = "body", type = "LongText", required = true },
{ name = "status", type = "Enum", values = ["draft", "published"], default = "draft" },
{ name = "authorId", type = "Ref", ref = "User.id", index = true },
{ name = "createdAt", type = "DateTime", default = "now" }
]
TypeDescription
ULIDPrimary key, auto-generated unique identifier
UUIDUUID identifier
TextShort text (VARCHAR)
LongTextLong text (TEXT)
EmailEmail address with validation
IntegerWhole numbers
FloatDecimal numbers
Booleantrue/false
DateTimeDate and time
DateDate only
JSONJSON data
EnumEnumerated values (use values = [...])
RefForeign key reference (use ref = "Entity.field")
OptionTypeDescription
primary_keybooleanMark as primary key
uniquebooleanEnforce uniqueness
indexbooleanCreate database index
requiredbooleanCannot be null
nullablebooleanExplicitly allow null
defaultanyDefault value ("now" for DateTime)
valuesstring[]Allowed values for Enum type
refstringForeign key target for Ref type
[entity.User.relations]
posts = { type = "hasMany", entity = "Post", foreign_key = "authorId" }
[entity.Post.relations]
author = { type = "belongsTo", entity = "User", foreign_key = "authorId" }

Relation types: hasMany, hasOne, belongsTo, manyToMany (requires through).

Define who can read, create, update, or delete records:

# Simple boolean
[entity.Post.access]
read = true
create = true
update = { authorId = "$currentUser.id" }
delete = { "$currentUser.role" = "admin" }

Use $currentUser to reference the logged-in user’s fields. Combine conditions with or/and:

[entity.Post.access]
read = { or = [{ status = "published" }, { authorId = "$currentUser.id" }] }

Field-level access:

[entity.User]
fields = [
{ name = "email", type = "Email", access = { read = true, write = { "$currentUser.role" = "admin" } } }
]

Pages bind URL paths to layouts and queries.

Pages can also mount interactive widgets directly under [page."/path".widget]. For the current widget types and examples, see Widgets, Board Widget, and Lookup Widget.

LayoutDescription
listTable view of query results
detailSingle record display
formCreate/update/delete forms
dashboardWidgets and summary cards
authAuthentication pages (sign-in)
[page."/posts"]
title = "All Posts"
auth = "none"
layout = "list"
[page."/posts".query.posts]
entity = "Post"
where = { status = "published" }
orderBy = { createdAt = "desc" }
limit = 20
include = ["author"]
[page."/posts/:id"]
title = "Post Detail"
auth = "none"
layout = "detail"
[page."/posts/:id".query.post]
entity = "Post"
where = { id = "$params.id" }
include = ["author"]
[page."/"]
title = "Dashboard"
auth = "required"
layout = "dashboard"
[page."/".query.recentPosts]
entity = "Post"
orderBy = { createdAt = "desc" }
limit = 5
[page."/".query.totalUsers]
entity = "User"
OptionDescription
entityEntity to query
whereFilter conditions (use $params.id for URL params)
orderBySort order ({ field = "asc" } or "desc")
limitMaximum number of results
offsetSkip N results
includeRelated entities to include

Widgets are page-level interactive views that live alongside standard page configuration:

[page."/customers"]
title = "Customer Search"
[page."/customers".widget]
kind = "lookup"
entity = "Customer"
search = ["lastName", "firstName", "email"]

Use widgets when a page should behave more like a tool than a traditional CRUD screen. Zebric currently supports:

  • board for Kanban-style issue and workflow pages
  • lookup for standalone search-and-select pages

For full widget configuration and examples, see Widgets, Board Widget, and Lookup Widget.

Forms handle record creation and updates.

[page."/posts/new"]
title = "New Post"
auth = "required"
layout = "form"
[page."/posts/new".form]
entity = "Post"
method = "create"
[[page."/posts/new".form.fields]]
name = "title"
type = "text"
label = "Title"
required = true
[[page."/posts/new".form.fields]]
name = "category"
type = "select"
label = "Category"
options = [
{ value = "general", label = "General" },
{ value = "finance", label = "Finance" }
]
[[page."/posts/new".form.fields]]
name = "body"
type = "textarea"
label = "Content"
rows = 10
required = true
[page."/posts/new".form.onSuccess]
redirect = "/posts/{id}"
message = "Post created!"

text, textarea, email, password, number, select, checkbox, radio, file, date, datetime

OptionDescription
labelDisplay label
placeholderPlaceholder text
requiredField is required
defaultDefault value
optionsChoices for select/radio (string array or { value, label } objects)
rowsNumber of rows for textarea
patternRegex validation pattern
error_messageCustom error message
min / maxNumber range constraints
acceptAccepted file types for file input

Detail pages can include an action bar with workflow triggers:

[page."/issues/:id"]
title = "Issue Detail"
layout = "detail"
[page."/issues/:id".actionBar]
statusField = "status"
[[page."/issues/:id".actionBar.actions]]
label = "Set Status"
workflow = "SetIssueStatus"
style = "primary"
[[page."/issues/:id".actionBar.actions]]
label = "Request Approval"
workflow = "RequestApprovalIfNeeded"
style = "secondary"

Customize layout rendering with named slots:

LayoutAvailable Slots
listlist.header, list.body, list.empty
detaildetail.main, detail.related
formform.form
dashboarddashboard.widgets
[ui]
render_mode = "server"
theme = "default"
progressive_enhancement = "none" # "none", "alpine", or "htmx"
view_transitions = true
[ui.tailwind]
primary_color = "#3B82F6"

The renderer ships two Theme objects — defaultTheme (light) and darkTheme — exported from @zebric/runtime-core. The theme field above is accepted by the schema but not yet wired up to select between them automatically; applying a different theme currently requires passing a Theme object to the renderer programmatically rather than by name in the blueprint.

Any value other than "none" enables client-side form enhancement: forms render with a data-enhance attribute, and a small inline script intercepts submits, does an AJAX fetch with CSRF-token forwarding, shows a loading state on the submit button, and redirects or displays inline field errors based on the JSON response — no full page reload. The "alpine" and "htmx" values are accepted by the schema but don’t currently load those libraries; they behave the same as any non-"none" value.

view_transitions = true adds a <meta name="view-transition"> tag and @view-transition CSS so supporting browsers animate page navigations.

Every page responds with HTML by default, or JSON if the request’s Accept header includes application/json without also including text/html:

Terminal window
curl http://localhost:3000/posts # HTML
curl -H "Accept: application/json" http://localhost:3000/posts # JSON

Rendered pages include WCAG-oriented features: a skip-to-content link, aria-label/aria-current on navigation, table <caption> and scope="col" headers, aria-describedby/aria-invalid/role="alert" on form validation errors, and an sr-only utility class for screen-reader-only content.

[auth]
providers = ["email"]
trustedOrigins = ["http://localhost:3000"]
[[auth.apiKeys]]
name = "my-agent"
keyEnv = "MY_API_KEY"
[notifications]
default = "console"
[[notifications.adapters]]
name = "console"
type = "console"
[[notifications.adapters]]
name = "slack_ops"
type = "slack"
[notifications.adapters.config]
botTokenEnv = "SLACK_BOT_TOKEN"
defaultChannel = "#ops"

Adapter types: console, slack, email.

Use the framework stories package to test your blueprints:

Terminal window
pnpm --filter @zebric/framework-stories test

See the examples/ directory for complete blueprint files.