Library mode renders application-owned admin modules inside the host Phoenix endpoint. It is Incant's default mode: the Incant OTP application starts no children when serve?: false.

The host owns:

  • endpoint, sessions, and authentication;
  • Ecto repos and application supervision;
  • route placement and network access;
  • CSS tokens and browser assets;
  • admin modules, policies, queries, and side effects.

Admin root

Register resources, dashboards, and datasets on one root:

defmodule MyApp.Admin do
  use Incant.Admin,
    repo: MyApp.Repo,
    theme: MyApp.Admin.Themes.Default,
    policy: MyApp.Admin.Policy,
    actor_assign: :current_scope,
    naming: [terms: %{openai: "OpenAI", api: "API"}]

  resource MyApp.Admin.Resources.Product
  resource MyApp.Admin.Resources.Order
  dashboard MyApp.Admin.Dashboards.Operations
  dataset MyApp.Admin.Datasets.Sales
end

Root options become defaults for registered surfaces. A resource or dashboard can override its repo or policy when ownership differs.

Router and authentication

defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  use Incant.Router

  scope "/", MyAppWeb do
    pipe_through [:browser, :require_authenticated_user, :require_operator]
    incant "/admin", MyApp.Admin
  end
end

Use the same session and authentication pipeline as the rest of the Phoenix application. Incant reads the configured actor assign from LiveView assigns and passes it into policy callbacks.

If actor_assign is omitted, Incant checks common assign names in this order:

current_scope
current_user
current_admin
actor
user

See Authorization for explicit extraction and row scoping.

Assets

Add Incant to Tailwind scanning:

@source "../deps/incant/lib";

Let Volt resolve the Hex dependency as a frontend package:

config :volt,
  resolve_dirs: ["deps"]

Then import and mount browser behavior from the host entry:

import { mountIncant } from "incant"

mountIncant()

The regular package entry contains UI behavior only. It deliberately does not connect a LiveSocket, so the host keeps its existing Phoenix socket setup.

Schema-backed resources

An Ecto resource can infer fields and use standard query operations:

defmodule MyApp.Admin.Resources.Order do
  use Incant.Resource,
    schema: MyApp.Sales.Order,
    repo: MyApp.Repo

  table do
    column :number, link: true
    column :status, as: :badge
    column :total, format: :money
    column :inserted_at, format: :datetime

    filter :status, :multi_select, options: [:pending, :paid, :shipped]
    filter :inserted_at, :date_range
    search [:number, :email]
  end

  form do
    field :status, :select, options: [:pending, :paid, :shipped]
    field :notes, :textarea
  end
end

Incant casts built-in filter values through the Ecto field type, applies policy query scoping, and passes the resulting table state through search, filters, sort, count, and pagination.

Query-backed resources

Use application queries when a surface needs joins, aggregates, projections, or a nonstandard schema:

defmodule MyApp.Admin.Resources.CustomerHealth do
  use Incant.Resource,
    title: "Customer Health"

  index &MyApp.Admin.Queries.customer_health/2
  read &MyApp.Admin.Queries.customer/2

  table do
    column :customer, link: true
    column :plan
    column :monthly_spend, format: :money
    column :last_seen_at, format: :relative
  end
end

Keep domain query logic in application modules. Resource metadata should describe presentation and available operations rather than become a second data layer.

Forms and actions

Resource forms use declared fields or infer compatible schema fields. Create and update still run through application callbacks or Ecto changesets.

Actions can return normal success/error results, navigation, or one-time secret reveals:

action :rotate_key,
  callback: &MyApp.Keys.rotate/2,
  confirm: "Rotate this key?",
  destructive: true

Use Incant.ActionResult.Reveal for a newly generated secret that should appear once in a protected dialog.

UI configuration

The default adapter is Incant.UI.Adapters.LiveView:

config :incant,
  ui_adapter: Incant.UI.Adapters.LiveView,
  density: :compact

Per-admin overrides use the admin module as the application config key:

config :incant, MyApp.Admin,
  density: :comfortable

Theme tokens remain host CSS variables. See Themes and LiveView.

Testing

Admin metadata is ordinary Elixir data:

metadata = Incant.metadata(MyApp.Admin)
assert MyApp.Admin.Resources.Order in metadata.resources

Use Phoenix LiveView tests for router/session rendering and focused unit tests for query callbacks, policies, action handlers, and formatters. You do not need a browser to verify the semantic contract.