mix caravela.gen.live MyApp.Domains.Library generates a working
Svelte frontend for every entity in the domain. Each entity picks one
of two render modes via its DSL declaration:
frontend: :live(default) — Phoenix LiveView + WebSocket.frontend: :rest— Phoenix controller + Inertia-style HTTP.
Both modes mount Svelte components through
caravela_svelte, so the
same BookIndex.svelte works under either transport without
changes — the author barely notices which mode is in play.
Setup
Add caravela_svelte to the consumer app:
# mix.exs
{:caravela, "~> 0.13"},
{:caravela_svelte, "~> 0.1"}Wire the client runtime into assets/js/app.js following the
caravela_svelte docs.
Generated files
lib/my_app_web/live/library/book_live/{index,show,form}.ex # :live mode
lib/my_app_web/controllers/book_controller.ex # :rest mode
assets/svelte/library/{BookIndex,BookShow,BookForm}.svelte # both modes
assets/svelte/library/{BookIndex,BookShow,BookForm}.test.ts # Vitest smoke tests
assets/svelte/types/library.ts # shared TS
test/my_app_web/live/library/book_live_test.exs # :live tests
test/my_app_web/controllers/book_controller_test.exs # :rest testsThe trio of Svelte components is identical between modes. Mode is a server-side decision; the Svelte component receives the same props either way.
Registering routes
Don't paste route snippets. Caravela.Router exposes a compile-time
macro that expands to the full route list based on each entity's
frontend / realtime? declaration:
defmodule MyAppWeb.Router do
use Phoenix.Router
use Caravela.Router
import CaravelaSvelte.Router # needed only when any entity is :rest
scope "/", MyAppWeb.Library do
pipe_through :browser
caravela_routes MyApp.Domains.Library
end
endcaravela_routes/1 expands into live routes for :live entities
and caravela_rest routes for :rest entities, with
realtime: true appended automatically when the entity opts in.
Versioned domains (version "v1") insert the version segment into
the module aliases so Phoenix's scope alias resolution continues to
match the generated MyAppWeb.V1.Library.BookLive.Index layout.
caravela_routes MyApp.Domains.Library,
session: [on_mount: {MyAppWeb.Auth, :require_user}]Passing :session wraps the :live routes in a live_session/3
block so a group of entities can share an on_mount hook.
:live mode — what the generated LiveView looks like
defmodule MyAppWeb.Library.BookLive.Index do
use MyAppWeb, :live_view
alias MyApp.Library
def mount(_params, _session, socket) do
context = build_context(socket)
socket =
socket
|> assign(:context, context)
|> assign(:field_access, Library.field_access(:books, context))
|> assign(:actions, Library.action_access(:books, context))
|> assign(:books, Library.list_books(context))
|> assign(:loading, false)
|> assign(:flash_message, nil)
{:ok, socket}
end
def render(assigns) do
~H"""
<CaravelaSvelte.svelte
name="library/BookIndex"
props={%{
books: @books,
loading: @loading,
flash_message: @flash_message,
field_access: @field_access,
actions: @actions
}}
socket={@socket}
/>
"""
end
endThe LiveView is standard Phoenix — plain mount/3, explicit
handle_event clauses, the context module aliased at the top.
No Caravela runtime coupling in the default output.
:rest mode — what the generated controller looks like
defmodule MyAppWeb.BookController do
use Phoenix.Controller, formats: [:html]
alias MyApp.Library
alias Caravela.ChangesetTranslator
alias CaravelaSvelte.Caravela, as: CS
def index(conn, _params) do
context = build_context(conn)
items = Library.list_books(context)
field_access = field_access(context)
actions = Library.action_access(:books, context)
conn
|> CS.put_field_access(field_access)
|> CaravelaSvelte.render("library/BookIndex", %{
books: items,
field_access: field_access,
actions: actions
})
end
# … show / new / edit / create / update / delete …
endCaravelaSvelte.render/3 produces an Inertia-compatible HTTP
response (JSON for SPA navigation, full HTML on first load).
Structured changeset errors flow through Caravela.ChangesetTranslator
(see validation).
realtime: true for :rest entities
Declaring realtime: true on a :rest entity:
entity :books, frontend: :rest, realtime: true do
field :title, :string, required: true
end… adds three things to the generated controller:
CS.broadcast_patch/3on create / update / delete, keyed by the conventional per-actor topic"caravela:<entity>:actor:<id>".- JSON-Patch operations matching the list mutation (
add/replace/removeon/<plural>/<id>). - A matching
caravela_rest "/books", BookController, realtime: trueline in the router expansion, whichCaravelaSvelte.Routerwires into the SSE endpoint clients subscribe through.
:live entities already have LiveView's WebSocket for real-time, so
realtime: true on a :live entity is rejected at compile time with
a clear error.
Version + multi-tenant flow-through
- Versioned domain →
MyAppWeb.V1.Library.BookLive.Indexunderlib/my_app_web/live/v1/…, mountingv1/library/BookIndex.svelte. The Router macro inserts theV1.segment automatically. - Multi-tenant domain → generated
build_context/1picks upsocket.assigns[:tenant]/conn.assigns[:tenant]automatically, so tenant scoping in the context Just Works.
The --with-domain flag (:live mode only)
For an onramp to the more ambitious Live runtime,
pass --with-domain:
mix caravela.gen.live --with-domain MyApp.Domains.Library
This emits one extra file per :live entity — a
Caravela.Live.Domain-backed companion module — and regenerates
form.ex to use Caravela.Live.Template. Index and show stay plain.
The Template-backed form is ~30% shorter and makes the Updater model
concrete. :rest entities are unaffected.
Other flags
--frontend rest|live— blanket override. Treats every entity in the domain as the given mode, ignoring DSL declarations. Useful for previewing generator output.--no-tests— skip emitting the ExUnit + Vitest test skeletons (they're emitted by default — see testing).--dry-run,--force,--output DIR— standard across all generators.
Client events (:live mode)
Svelte components receive live as a prop and call live.pushEvent
to dispatch events back. The generated components use these event
names:
| Component | Event | Params | What the server does |
|---|---|---|---|
| Index | new | {} | push_navigate to /new |
| Index | edit | {id} | push_navigate to /<id>/edit |
| Index | delete | {id} | Library.delete_book/2 + refetch list |
| Show | edit | {id} | push_navigate to /<id>/edit |
| Show | back | {} | push_navigate to index |
| Form | validate | {field, value} | Rebuild changeset, update :errors |
| Form | save | {} | create_* or update_*, navigate |
| Form | cancel | {} | push_navigate to index |
Feel free to add your own — generated handle_event clauses live
above the # --- CUSTOM --- marker; anything you add below it
survives regeneration (see regeneration).
Client navigation (:rest mode)
:rest pages use Inertia-style SPA navigation through
caravela_svelte's client helpers (navigate, useForm). See the
caravela_svelte docs for the
full client API.
Props your Svelte component sees
For BookIndex.svelte:
let {
books = [],
loading = false,
flash_message = null,
field_access = { title: true, isbn: true /* … */ },
actions = { create: true, update: true, delete: true },
live // :live mode only; absent under :rest
}: {
books?: Book[];
loading?: boolean;
flash_message?: string | null;
field_access?: BookFieldAccess;
actions?: BookActions;
live?: LiveHandle;
} = $props();For BookForm.svelte:
let {
book = {},
errors = {},
saving = false,
field_access = { /* … */ },
actions = { create: true, update: true, delete: true },
live
}: {
book?: Partial<Book>;
// Structured error shape from Caravela.ChangesetTranslator.
// See docs/validation.md.
errors?: Record<string, Array<{ code: string; params: object; message: string }>>;
saving?: boolean;
field_access?: BookFieldAccess;
actions?: BookActions;
live?: LiveHandle;
} = $props();The TypeScript interfaces are regenerated from the domain IR. Changes
to field :title, :string, required: true in Elixir immediately flow
to title: string (no ?) in TypeScript on the next mix caravela.gen.live. See policies for how
BookFieldAccess and BookActions relate to the policy block.
SSR caveats
Both modes render components server-side via caravela_svelte's Node
bridge by default. A few Svelte libraries load code lazily at runtime
in a way the Node bridge can't resolve:
Shiki (syntax highlighter) — dynamically imports per-language grammar chunks. Under Node SSR the dynamic imports fail and the render crashes. Workaround: disable SSR in your app config:
# config/config.exs config :caravela_svelte, :ssr, falseClient-side hydration still works; only the initial server render falls back to a blank placeholder until the Svelte runtime takes over.
If you hit a similar "works in the browser, crashes in SSR" pattern with another library, the same switch applies.