Every Caravela domain is a module that uses Caravela.Domain. The
DSL declares entities, relations, lifecycle hooks, and authorization
rules; the compiler builds an IR and validates it before generators
run.
entity :<name> do ... end
Declares one entity (one database table). The name is plural
(:books); the generator derives a singular module name (Book), a
plural table name (library_books), and a path
(lib/<app>/library/book.ex).
entity :books do
field :title, :string, required: true
field :isbn, :string
endfield :<name>, <type>, opts
| option | applies to | effect |
|---|---|---|
required | any | null: false + validate_required |
default | any | column default |
min, max | numeric | validate_number |
min_length, max_length | string-like | validate_length |
format | string-like | validate_format (regex) |
precision, scale | numeric | decimal precision/scale |
Recognised types: :string, :text, :integer, :bigint,
:float, :decimal, :boolean, :date, :time, :naive_datetime,
:utc_datetime, :binary, :binary_id, :uuid, :map, :json,
:jsonb.
relation :<from>, :<to>, type: <t>
t is one of :has_many, :has_one, :belongs_to, :many_to_many.
Declare either side of a relationship — Caravela infers the other.
relation :authors, :books, type: :has_many
relation :books, :publishers, type: :belongs_to, required: truerequired: true on a :belongs_to produces null: false on the FK
and on_delete: :delete_all in the migration.
version "v<n>" (optional)
Declares the domain's API version. When set, generated modules and routes are namespaced under the version segment. See versioning.
use Caravela.Domain
version "v1"use Caravela.Domain, multi_tenant: true (optional)
Opts the domain into row-level multi-tenancy: a :tenant_id
(:binary_id, null: false) field is auto-injected into every
entity, and the generated context scopes reads/writes by
context.tenant.id. See multi-tenancy.
Hooks: on_create, on_update, on_delete
Hooks run inside the generated context, between authorization and the
final Repo call:
on_create :books, fn changeset, context -> ... end # → changeset
on_update :books, fn changeset, context -> ... end # → changeset
on_delete :authors, fn author, context -> ... end # → :ok | {:error, reason}context is whatever map you pass to the context function. In the
generated controllers it defaults to %{current_user: …, conn: conn}
(plus tenant: when multi_tenant: true).
If {:error, reason} is returned from on_delete, the delete is
aborted and the tuple propagates back to the caller.
Authorization: policy blocks
Caravela's authorization is declared via policy :entity do … end
blocks. A single policy compiles into three enforcement targets —
Ecto WHERE clauses, field-level projection on API responses, and a
typed field_access Svelte prop — so UI, API, and database stay in
sync automatically. See Policies for the full guide.
policy :books do
scope fn query, actor ->
if actor.role == :admin, do: query, else: where(query, [b], b.published)
end
field :price, visible: fn actor -> actor.role in [:admin, :editor] 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
endTo use query macros like where / from inside scope, add
import Ecto.Query at the top of your domain module.
Compile-time validations
Every rule raises a CompileError pointing at the offending line:
- Unknown field types (
:widgetetc.) - Numeric constraints on non-numeric fields (and vice versa)
- Duplicate entity names
- Relations referencing undeclared entities
- Incompatible cardinality (e.g. both sides
:has_many) - Circular chains of required
belongs_to(unsatisfiable inserts) - Hooks / policy rules with the wrong function arity
- Hooks / policies referring to unknown entities or fields
- Duplicate hook for the same (action, entity), or duplicate
policyblock - Version strings that don't match
~r/^v\d+$/ - Manual
tenant_idfields in amulti_tenant: truedomain