DSL entry point. use Caravela.Domain in a module to declare a domain:
defmodule MyApp.Domains.Library do
use Caravela.Domain
entity :authors do
field :name, :string, required: true
field :bio, :text
end
entity :books do
field :title, :string, required: true, min_length: 3
field :isbn, :string, format: ~r/^\d{13}$/
end
relation :authors, :books, type: :has_many
on_create :books, fn changeset, _context ->
Ecto.Changeset.validate_required(changeset, [:title])
end
policy :books do
scope fn q, actor ->
if actor.role == :admin, do: q, else: where(q, [b], b.published)
end
allow :create, fn actor -> actor.role in [:admin, :editor] end
end
endAfter compilation the module exposes __caravela_domain__/0, returning
the validated Caravela.Schema.Domain IR, plus __caravela_hook__/4
and the __caravela_policy_*__ clauses emitted by each policy block.
Summary
Functions
Declare an action gate for the enclosing policy block. action is
one of :create, :update, :delete; the fn has arity 1
(actor -> bool) or arity 2 (actor, record -> bool).
Declare the authenticatable trait on the enclosing entity.
Enable email confirmation inside an authenticatable block.
Declare an entity (a table/schema) with a do block of fields.
Declare a field. Two call shapes are supported and dispatched on the second argument's AST
Declare a create-time hook for an entity. The hook receives the
Ecto.Changeset and the caller's context map, and must return an
Ecto.Changeset.
Declare a delete-time hook. The hook receives the loaded entity and
the context, and must return :ok or {:error, reason}.
Custom post-login logic. Receives the loaded user and the caller's
context, returns :ok or {:error, reason}.
Custom registration logic. Receives the changeset and the caller's context map, returns the changeset.
Declare an update-time hook. Same shape as on_create/2.
Declare a triple-target policy for entity.
Declare a relation between two entities.
Enable password reset inside an authenticatable block.
Declare the row-level scope for the enclosing policy block.
Configure session management inside an authenticatable block.
Declare a credential strategy inside an authenticatable block.
Declare the API version for this domain. Must match ~r/^v\d+$/.
Functions
Declare an action gate for the enclosing policy block. action is
one of :create, :update, :delete; the fn has arity 1
(actor -> bool) or arity 2 (actor, record -> bool).
allow :create, fn actor -> actor.role in [:admin, :editor] end
allow :update, fn actor, record -> actor.id == record.author_id end
Declare the authenticatable trait on the enclosing entity.
entity :users do
field :email, :string, required: true, unique: true
field :name, :string, required: true
authenticatable do
strategy :password
strategy :api_token, scopes: [:read, :write], ttl: {90, :days}
session :token, ttl: {30, :days}, remember_me: {365, :days}
confirm :email, token_ttl: {24, :hours}
reset :password, token_ttl: {1, :hour}
on_register fn changeset, _ctx -> changeset end
on_login fn user, _ctx ->
if user.suspended, do: {:error, :suspended}, else: :ok
end
end
endSee Caravela.Schema.AuthConfig for the parsed IR.
Enable email confirmation inside an authenticatable block.
Declare an entity (a table/schema) with a do block of fields.
entity :books do
field :title, :string, required: true
endEntity-level options may be passed between the name and the do
block:
entity :books, frontend: :rest do
field :title, :string, required: true
endSupported options:
:frontend— render transport for generated UI. One of:live(default — LiveView + WebSocket) or:rest(Inertia-style HTTP viacaravela_svelte). Entities stay on:livewhen the option is omitted, so existing domains are unaffected.:realtime— opt into SSE-driven live updates on top of the:resttransport. Defaults tofalse. Only valid whenfrontend: :rest—:liveentities already have LiveView's WebSocket, sorealtime: truethere is rejected with aCaravela.DSLError. Generated controllers get abroadcast_patch/3call site on create / update / delete.entity :books, frontend: :rest, realtime: true do field :title, :string, required: true end
Declare a field. Two call shapes are supported and dispatched on the second argument's AST:
Entity field (inside entity do … end): second arg is a type
atom.
field :title, :string, required: true, min_length: 3Policy field rule (inside policy do … end): second arg is a
keyword list with :visible.
field :price, visible: fn actor -> actor.role == :admin end
field :author_email,
visible: fn actor, record -> actor.id == record.author_id endThe dispatch is a pure AST check — no runtime overhead.
Declare a create-time hook for an entity. The hook receives the
Ecto.Changeset and the caller's context map, and must return an
Ecto.Changeset.
on_create :books, fn changeset, _context ->
Ecto.Changeset.validate_required(changeset, [:title])
end
Declare a delete-time hook. The hook receives the loaded entity and
the context, and must return :ok or {:error, reason}.
on_delete :authors, fn author, _context ->
if author.published?, do: {:error, :has_published_books}, else: :ok
end
Custom post-login logic. Receives the loaded user and the caller's
context, returns :ok or {:error, reason}.
Custom registration logic. Receives the changeset and the caller's context map, returns the changeset.
Declare an update-time hook. Same shape as on_create/2.
on_update :books, fn changeset, _context ->
changeset
end
Declare a triple-target policy for entity.
policy :books do
scope fn query, actor ->
if actor.role == :admin, do: query, else: where(query, [b], b.published)
end
field :internal_notes, visible: fn actor -> actor.role == :admin end
field :author_email, visible: fn actor, record ->
actor.role == :admin or actor.id == record.author_id
end
allow :create, fn actor -> actor.role in [:admin, :editor] end
allow :update, fn actor, record ->
actor.role == :admin or actor.id == record.author_id
end
allow :delete, fn actor -> actor.role == :admin end
endThe block is plain Elixir — for, if, helper function calls, and
@module_attribute splicing all work the same as anywhere else:
policy :books do
for f <- @admin_only_fields do
field f, visible: fn actor -> actor.role == :admin end
end
if Mix.env() == :dev do
allow :delete, fn _actor -> true end
end
endEach rule declaration stores metadata in the @caravela_policy_rules
accumulator; Caravela.Compiler.__before_compile__/1 then assembles
the full IR and emits the __caravela_policy_*__ dispatch clauses
in one pass.
Declare a relation between two entities.
relation :authors, :books, type: :has_many
relation :books, :publishers, type: :belongs_to
Enable password reset inside an authenticatable block.
Declare the row-level scope for the enclosing policy block.
scope fn query, actor ->
if actor.role == :admin, do: query, else: where(query, [b], b.published)
endThe fn must have arity 2 — (query, actor) -> query. Raises at
compile time if called outside a policy block.
Configure session management inside an authenticatable block.
Declare a credential strategy inside an authenticatable block.
Declare the API version for this domain. Must match ~r/^v\d+$/.
version "v1"When set, all generated modules are namespaced under the version
(MyApp.Library.V1.Book) and controller routes are prefixed with
/api/v1/.