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
end

field :<name>, <type>, opts

optionapplies toeffect
requiredanynull: false + validate_required
defaultanycolumn default
min, maxnumericvalidate_number
min_length, max_lengthstring-likevalidate_length
formatstring-likevalidate_format (regex)
precision, scalenumericdecimal 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: true

required: 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
end

To 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:

  1. Unknown field types (:widget etc.)
  2. Numeric constraints on non-numeric fields (and vice versa)
  3. Duplicate entity names
  4. Relations referencing undeclared entities
  5. Incompatible cardinality (e.g. both sides :has_many)
  6. Circular chains of required belongs_to (unsatisfiable inserts)
  7. Hooks / policy rules with the wrong function arity
  8. Hooks / policies referring to unknown entities or fields
  9. Duplicate hook for the same (action, entity), or duplicate policy block
  10. Version strings that don't match ~r/^v\d+$/
  11. Manual tenant_id fields in a multi_tenant: true domain