All notable changes to Caravela are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
[0.8.0] — 2026-04-18
Changed (breaking)
Deny-by-default policy fallback. Domains now default to
default_policy: :deny. Any entity without a declaredpolicyblock has its scope filtered to zero rows, every field masked out, and every write denied. The prior permissive behavior is still available asuse Caravela.Domain, default_policy: :allow.Motivation: a forgotten policy on a new entity used to silently ship unscoped data. With deny-by-default, the same forgetful moment produces an obviously-empty list instead of a leak. If you want per-entity control, add a minimal
policy :entity do scope fn q, _ -> q end end— declaring any policy block for an entity makes its undeclared rule types permissive for that entity (the "per-entity fallback" tier of the cascade).Generated
compute_field_access/2routes every field through the policy dispatch function. Previously, unruled fields were hardcoded to literaltruein the generated context; the hardcoding hid the domain-leveldefault_policyfrom reaching them. Now every field calls__caravela_policy_field_visible__/3and the clause cascade on the domain module (specific rule → per-entity fallback → module-level fallback) decides the result. No visible runtime difference for domains without policies that stayed on:allow.
Added
Caravela.Schema.Domain.default_policy/1helper returning:denyor:allow.use Caravela.Domainnow importsEcto.Query.where/3andEcto.Query.from/2into the calling module, soscope fn q, actor -> where(q, [b], b.published) endworks without a manualimport Ecto.Query(previously a silent runtime failure).
Migration
- If any of your domains rely on the old permissive fallback, add
default_policy: :allowto theuse Caravela.Domaincall. - If you want to tighten up an existing domain, remove the
:allowoption and add apolicy :entity do …block per entity. Missing rule types inside the block stay permissive, so partial declarations are safe.
[0.7.0] — 2026-04-18
Removed (breaking)
can_read/can_create/can_update/can_deletemacros and the__caravela_permission__/2,3,4dispatch functions they emitted. These ran in parallel with phase 9'spolicyblocks, producing an implicit intersection of two authorization systems with no documentation of the interaction. Since the library has no published users yet, the simpler path was to delete outright instead of deprecating.Caravela.Schema.Permissionstruct and thepermissionsfield onCaravela.Schema.Domain.- Generated context helpers
apply_read_permission/3,authorize_create/2,authorize_update/3,authorize_delete/3. Read paths now go throughapply_scope/3; write paths go throughpolicy_authorize/3,4. Both resolve against thepolicyblock's compiled dispatch functions on the domain module.
Migration
Port every can_* declaration into a policy :entity do … end block.
Rule functions now receive the actor (context.current_user),
not the raw context map:
# before
can_read :books, fn q, ctx -> where(q, [b], b.published) end
can_create :books, fn ctx -> ctx.current_user.role == :admin end
can_update :books, fn b, ctx -> ctx.current_user.id == b.author_id end
can_delete :books, fn _b, ctx -> ctx.current_user.role == :admin end
# after
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 == :admin end
allow :update, fn actor, record -> actor.id == record.author_id end
allow :delete, fn actor -> actor.role == :admin end
endThe generated context is the one that did the dispatch, so the only runtime change is the shape of the predicate's first argument.
[0.6.0] — 2026-04-18
Added
- Phase 7 —
authenticatabletrait. Declareauthenticatableon an entity andmix caravela.gen.authemits the full email/password- API-token server stack: auth context (register/login/logout,
sessions with TTL +
remember_me+max_sessions, email confirmation, password reset, scoped API tokens), session schema, Plug pipeline (fetch_current_user,require_auth,require_role,require_scope), LiveViewon_mounthooks, auth controller, and a session-tokens migration.on_register/on_loginlifecycle callbacks. Validated compile-time: one authenticatable entity per domain,:emailfield required when:passwordstrategy is used, no collision with auto-injectedhashed_password/confirmed_at/api_tokens.
- API-token server stack: auth context (register/login/logout,
sessions with TTL +
- Phase 8 — LiveSvelte auth UI.
mix caravela.gen.authalso emits six Svelte components —LoginForm,RegisterForm(inputs derived from the user entity's public fields),ResetPasswordForm(two-phase: request + confirm),ConfirmEmail,TokenManager(scopes +max_tokensreflected from the DSL),SessionList— plus matching LiveView pages (AuthLive.*), and prints a ready-to- paste router snippet (public auth routes + authenticatedlive_sessionwithon_mounthook + authenticated API scope). Generated User schema gainsregistration_changeset,password_changeset,api_tokens_changeset,confirm_changesetwith Argon2 password hashing. TS types addCurrentUser,ApiToken,Session. Flags--skip-ui/--skip-routerfor server-only use. - Phase 9 — Triple-target policies. New
policy :entity do … endblock in the domain DSL compiles a single declaration into three simultaneous enforcement layers: (1) EctoWHEREclauses viascope fn query, actor -> query end, (2) field projection that strips invisible fields from everylist_*/get_*response viafield :name, visible: fn actor[, record] -> bool end, and (3) a typedfield_accessSvelte prop (<Entity>FieldAccessTypeScript interface) that the generated index/show/form components use to gate ruled columns, fields, and inputs with{#if field_access.*}.allow :create | :update | :delete, fn actor[, record] -> bool endextends the Phase 2 permission check with record-aware gates. Arity-1 rules resolve to booleans at request time; arity-2 rules resolve to a'per_record'sentinel and evaluate per row, with denied fields served asnull. Unpolicied entities fall through to permissive defaults — fully backward-compatible with existingcan_*hooks. Caravela.Policymodule withEntry,Scope,FieldRule,ActionGateIR structs, andDomain.policy_for/2/auth_entity/1lookups.- New guides: docs/auth.md, docs/policies.md.
Changed
- Generated TypeScript type imports are now combined
(
import type { Book, BookFieldAccess, LiveHandle } from '…') instead of one line per type. - Generated
UserEcto schema excludeshashed_password,confirmed_at,api_tokensfrom the genericcastpath — they flow only through the specialised changesets above. Caravela.Gen.Svelte.public_fields_for/1also strips credential fields (hashed_password,api_tokens) from the Svelte-bound representation, so they can never reach the browser through either the typed TS interface or a CRUD form.
[0.5.3] — 2026-04-18
Added
Caravela.Error— uniform error struct (:unauthorized,:not_found,:invalid,:internal) so upstream code can pattern- match once instead of threading the four shapes individually generated contexts return today.wrap/1lifts a plain reason into the struct;message/1renders a default flash phrase.Caravela.Live.OnMount—on_mountcallback that assigns a:contextmap (%{current_user, tenant}) to the socket, so LiveViews can read from@contextinstead of each re-rollingbuild_context/1.put/3exposes a merge helper for downstream hooks.Caravela.Gen.Contextnow emits adelete_<entity>(id, context)variant alongside the existing(struct, context)form, so delete-from-index is one round trip (get |> delete). Returns{:error, :not_found}when the id is missing or hidden. The generated index LiveView uses the shorter path.
Changed
- Generated
--with-domainform LiveView now passes keyword args (apply_updater(:load, entity: e, attrs: a, errors: er)andapply_updater(:put_attr, field: f, value: v)) instead of anonymous tuples. The emittedFormDomainmatches withKeyword.fetch!/2. Self-documenting and survives adding a new field without silently shifting positional args.
Fixed
- Generated Svelte components now destructure the
livehook handle LiveSvelte ≥ 0.18 actually injects, and calllive.pushEvent(...). Previously every generated component pulledpushEventout of$props()— which never existed — so every Edit / Delete / New / Save / Cancel button silently threwTypeError: pushEvent is not a functionand the corresponding server event never fired. Caravela.Gen.Contextnow emitspreload([:assoc, …])onlist_*/get_*/get_*!for everybelongs_toassociation declared on the entity. Rows no longer ship the raw%Ecto.Association.NotLoaded{}sentinel (with__owner__/__field__/__cardinality__internals) to the browser, and dereferencingbook.author.nameon the Svelte side now works without a hand-written preload.- Generated
--with-domainand plain form LiveViews replace theMap.from_struct(entity) |> Map.drop([:__meta__])attrs seed with a narrowentity_attrs/1helper that keeps only the declared entity fields and normalises%Decimal{}values to their string form. Form inputs on an Edit screen no longer render[object Object]for decimal / money fields, and the attrs map has stable types across validate round-trips.
Changed
- Generated LiveView
render/1now calls<LiveSvelte.svelte>(passingsocket={@socket}) instead of the deprecated<LiveSvelte.render>component. Silences the deprecation warning on every request and re-enables SSR for connected sockets. - New
Caravela.Live.Encodersmodule providesLiveSvelte.Encoderprotocol implementations forDecimal(→ normalised string) andEcto.Association.NotLoaded(→nil), guarded so the file compiles even when LiveSvelte < 0.18 is in use (the protocol is absent there). - Generated TypeScript types file now exports a
LiveHandleinterface describinglive.pushEvent/pushEventTo/handleEvent, which every generated component imports for itsliveprop.
[0.5.2] — 2026-04-18
Changed
- Generated Svelte components and doc examples now use Svelte 5's
$props()rune for prop declarations instead of the deprecatedexport letsyntax. LiveSvelte 0.19 ships with Svelte 5 runtime;export letstill worked but produced compile-time warnings.
[0.5.1] — 2026-04-18
Fixed
- Generated
--with-domainform LiveView no longer crashes withKeyError :errorson mount. The template now seeds the domain's default state (viaCaravela.Live.Template.__assign_defaults__/2) before the firstapply_updater(:load, ...)call. Caravela.Flow.Runner—raceadvances as soon as the first task resolves instead of waiting the full timeout (was relying onTask.yield_many/2's "wait for all, take first").Caravela.Gen.Contextemits simplifiedauthorize_*/run_delete_hookfunctions when no correspondingcan_*/on_deleterule is declared, eliminating the "clause will never match" warnings that appeared on every compile of a fresh CRUD generation.Caravela.Gen.SvelteFormandCaravela.Gen.Sveltenow emit Svelte 5 event attribute syntax (onchange={...},oninput={...},onclick={...},onsubmit={...}) instead of the deprecatedon:eventdirective form.
Added
Caravela.Flow—:tagstart option. When set, every notification is delivered wrapped as{:caravela_flow, tag, original_msg}, letting a single listener driving many flows demultiplex without forwarder processes.
Changed
Caravela.Live.Domain/Caravela.Live.Form— when theupdater/on_event/visiblemacros reject a value they can't arity-check at compile time, the error message now points the reader at the accepted shapes (fn ... endor&Module.fun/N) and the wrap-it-in-fnworkaround for bound function variables.Caravela.Gen.Migrationmoduledoc now documents the:timestampoption for deterministic output (snapshot tests / demo pages). Behavior unchanged; only documentation.
[0.5.0] — 2026-04-18
Phase 5 — dynamic Svelte forms with server-driven visibility and
async validation, plus the Caravela.Flow GenServer runtime for
composable async workflows.
Added
Caravela.Live.Form— DSL layered onCaravela.Live.Domainfor form-visibility predicates (visible/2) and async field validators (validate_async/3). Each form-domain module exposes__caravela_form__/0,__caravela_form_visibility__/1,__caravela_form_visible__/2, and__caravela_form_validate_async__/3for introspection and runtime dispatch.Caravela.Gen.SvelteForm— generator that reads a form-domain module plus its owningCaravela.Schema.Domainand emits<Entity>FormDynamic.svelte. The component declaresfield_visibility/async_errorsprops, wraps guarded fields in{#if field_visibility.*}, and debounces async-validationpushEventcalls client-side.Caravela.Flow/Caravela.Flow.DSL—use Caravela.Flowplusflow/3,sequence,repeat,wait,wait_until,debounce,set_state,run,parallel,race, andeachmacros. Compiles to nested step-tree structs inCaravela.Flow.Steps.Caravela.Flow.Runner— GenServer interpreting step trees. Supports retry/backoff (linear + exponential),wait_untilthat unblocks onsignal/2,debouncethat resets on state change during the pause, parallel/race task orchestration, and per-itemeachiteration with{:ok|:skip|:error, _}returns.Caravela.Flow.Supervisor— optional DynamicSupervisor for flow runners.Caravela.Flow.start/3attaches runners when the supervisor is running, falls back to unsupervisedstart_linkotherwise (useful in tests and tooling).- Top-level API:
Caravela.Flow.start/3,Caravela.Flow.signal/2,Caravela.Flow.get_state/1,Caravela.Flow.stop/1,2. Flows deliver{:flow_state, _},{:flow_done, _}, and{:flow_error, _}messages to the:notifypid. docs/flows.md— new guide covering the flow DSL, primitives, the real-time loop through LiveView + LiveSvelte, and the scope boundary (no event sourcing).docs/live_runtime.md— extended withCaravela.Live.FormandCaravela.Gen.SvelteFormsections.
Scope
- Flows are strictly ephemeral: in-memory state, no persistence, no event sourcing. Teams needing durable event streams should use Commanded.
0.4.0 — 2026-04-17
Phase 4 — LiveView + typed Svelte component generation,
Caravela.Live.* runtime for composable state, and a docs restructure.
Added
mix caravela.gen.live— generates three LiveView modules per entity (index / show / form) plus matching typed Svelte components and a TypeScript interfaces file. LiveViews mount components via<LiveSvelte.render>and delegate CRUD to the generated context, so authorization, hooks, and multi-tenant scoping apply for free.Caravela.Gen.LiveView+Caravela.Gen.Svelte— EEx-backed generators that emit index/show/form templates. Both respect the# --- CUSTOM ---marker (TypeScript uses// --- CUSTOM ---, Svelte uses<!-- --- CUSTOM --- -->).Caravela.Gen.LiveRoute— prints aliverouter scope snippet with four routes per entity (index,:new,:show,:edit), analogous toCaravela.Gen.RouterScopefor the JSON API.--with-domainflag onmix caravela.gen.live— also emits aCaravela.Live.Domaincompanion module per entity and regeneratesform.exfrom a Template-backed variant. Index and show stay plain. Useful as an onramp to theCaravela.Live.*runtime.Caravela.Live.Updater— composable assigns-transformer helpers:run/2,3,compose/2,embed/2, and the~>pipe operator.Caravela.Live.Domain—usemacro withstate,updater,on_event, andon_infoDSL for server-side state machines. Compile-time checks enforce updater arity (1or2) and require string event names. Theuseblock sets@caravela_live_domain __MODULE__soapply_updater/2,3resolves inside domain bodies without an explicit module argument.Caravela.Live.Template—use Caravela.Live.Template, domain: Modbinds a LiveView to aLive.Domainmodule, injectingmount/3,handle_event/3,handle_info/2, andapply_updater/2,3. Unknown events log a warning instead of crashing; all callbacks aredefoverridable.- Naming helpers:
live_module/3,live_file_path/3,svelte_component_name/2,svelte_component_ref/3,svelte_file_path/3,svelte_types_file_path/1— all version-aware. - Documentation split into topic guides under
docs/(getting_started, dsl, generators, multi_tenancy, versioning, graphql, livesvelte, live_runtime, regeneration), wired intomix docsas ex_doc extras. README trimmed to a minimal entry point. - GitHub Actions workflow (
docs.yml) that deploysmix docsoutput to GitHub Pages on every push tomain. HexDocs continues to publish on tag release.
Changed
- BC-preserving rename.
Caravela.Live.Updater.apply/2,3→run/2,3to avoid shadowingKernel.apply/2,3.apply/2,3remains as an undocumented alias. - Generated Svelte
BookShow.sveltenow renders fields with the same null-safe expression as the index, so a missing field prints—instead ofundefined. - Generated Svelte
BookIndex.sveltenow includes a "New book" button that dispatchespushEvent('new', {}); the matchinghandle_eventnavigates to the form route.
Fixed
Caravela.Live.Domaindocstring previously showed an example that wouldn't compile:apply_updater/2,3was invoked insideon_eventbodies but the macro required@caravela_live_domain, which was only set byuse Caravela.Live.Template. Now set byCaravela.Live.Domaintoo.- Removed an unused
dirtylocal from the generated Svelte form.
0.3.0 — 2026-04-17
Phase 3 — multi-tenancy, API versioning, Absinthe/GraphQL generation.
Added
use Caravela.Domain, multi_tenant: true— opts into row-level multi-tenancy.Caravela.Tenantauto-injects a:tenant_id(:binary_id,null: false) field into every entity, and the generated context gainsscope_tenant/2+inject_tenant_id/2helpers driven bycontext.tenant.id.- Migrations in multi-tenant domains add the
tenant_idcolumn and composite[:tenant_id, :<fk>]indexes alongside each FK index, plus a standalone[:tenant_id]index on tables with no FKs. version "v1"DSL directive — all generated Elixir modules and file paths are namespaced under the version segment (MyApp.Library.V1.Book,MyAppWeb.V1.BookController,lib/my_app/library/v1/book.ex). The router snippet is emitted atscope "/api/v1", MyAppWeb.V1. Table names stay version-free so rows are shared across versions.- Two new compile-time validations: invalid version format (must match
~r/^v\d+$/) and manual:tenant_iddeclarations colliding with auto-injection. Caravela.Gen.GraphQL— renders Absinthe object types, query object, and mutation object (with typed input objects) for the domain. Every resolver delegates to the generated context, so authorization, hooks, and tenant scoping apply to GraphQL for free. Tenant-injected fields are hidden from both object and input types.mix caravela.gen.graphqltask — checks for Absinthe at runtime and prints an actionable error if the optional dependencies are missing.- Generated controllers read
conn.assigns[:tenant]into the context when the domain is multi-tenant.
0.2.0 — 2026-04-17
Phase 2 — hooks, permissions, Phoenix context + JSON API generators.
Added
- Hook DSL:
on_create/2,on_update/2,on_delete/2on any entity. Hooks run between authorization and the finalRepocall in the generated context.on_deletemay return{:error, reason}to abort the delete. - Permission DSL:
can_read/2,can_create/2,can_update/2,can_delete/2.can_readis applied as an Ecto query filter; the other three return booleans and afalseshort-circuits the context function with{:error, :unauthorized}. - Compiled domain modules expose
__caravela_hook__/4and__caravela_permission__dispatch functions with safe fallbacks. - Three new compile-time validations: hook / permission arity, unknown entity references, duplicate (action, entity) declarations.
Caravela.Gen.Context— Phoenix context generator with CRUD functions per entity (list_,get_,get_!,change_,create_,update_,delete_).Caravela.Gen.Controller— JSON controller generator (REST actions, standard status codes, changeset → 422 translation).Caravela.Gen.RouterScope— prints thescope "/api", MyAppWeb do … endsnippet to paste into the host app's router.Caravela.Gen.Custom— preserves user code below the# --- CUSTOM ---marker across regenerations. Schemas, contexts, and controllers all ship with the marker.- Mix tasks:
caravela.gen.context,caravela.gen.api, and the all-in-onecaravela.gen.
0.1.0 — 2026-04-17
Initial public release. Phase 1 — DSL, compiler, and schema/migration generators.
Added
Caravela.DomainDSL:entity,field,relation.Caravela.Compilerwith six compile-time validations: unknown field types, numeric-constraint/type mismatches, duplicate entities, dangling relation targets, incompatible cardinality, circular requiredbelongs_tochains.Caravela.Gen.EctoSchema— Ecto schema generator (with changeset, required/format/length/numeric validations).Caravela.Gen.Migration— Ecto migration generator, topologically sorted, with foreign-key indexes and appropriateon_deleterules derived fromrequired:.mix caravela.gen.schema MyApp.Domains.<Module>task with--dry-runand--forceoptions.:binary_idprimary and foreign keys by default.