Build Your First App
Copy MarkdownNew to Elixir, Phoenix, or Aurora UIX? Start here. This is a complete, copy-paste walkthrough that takes you from nothing installed to a running CRUD application in your browser — no prior Elixir or Phoenix experience required.
By the end you'll have a small Inventory app with two screens generated by Aurora UIX: a list of categories and a list of products, each with create, edit, and detail views, validation, and a category picker on the product form — all from a few lines of declarative configuration.
Already have a Phoenix app?
If you're an experienced Phoenix developer and just want to add Aurora UIX to an existing project, skip this tutorial and read the concise Getting Started guide instead.
Plan for ~20 minutes. Each step is a command or a block of code you can paste verbatim.
1. What you'll build
A two-table inventory manager:
- Category —
name,description. A category groups products. - Product —
name,price,stock, and a link back to a Category.
Aurora UIX reads metadata about these tables and generates the full UI: paginated lists, detail (show) pages, and create/edit forms — including a dropdown to pick a product's category, because it understands the relationship between the two tables.
You will:
- Install the toolchain (Elixir, Erlang, Phoenix).
- Create a fresh Phoenix app.
- Add the data layer — pick Phoenix + Ecto or Ash (both are shown).
- Add Aurora UIX and its assets.
- Describe the UI with Aurora UIX metadata.
- Add routes and run it.
2. Install the toolchain
Aurora UIX runs on Elixir (the language), which runs on Erlang/OTP (the virtual machine), inside a Phoenix web application. You need all three.
Target versions:
- Elixir
1.17+ - Erlang/OTP
28+ - Phoenix
1.8+
Recommended: use a version manager
If you're a seasoned developer, the cleanest way to install and pin Elixir and
Erlang is a version manager. It reads a .tool-versions file so every project
gets exactly the versions it expects — much better than a bare system install:
Install one of those, then add the Elixir and Erlang plugins and install the versions above. (We won't reproduce their setup steps here — follow the linked docs; it's a few commands.)
Or install directly
If you don't use a version manager, follow the official installers:
- Install Elixir (bundles Erlang on most platforms)
- Install Erlang/OTP (if your platform needs it separately)
Install the Phoenix project generator
Once Elixir is available, install the Phoenix app generator and Hex (the package manager):
mix local.hex
mix archive.install hex phx_new
Verify everything is in place:
elixir --version
mix phx.new --version
You also need PostgreSQL running locally — Phoenix uses it as the default database. Install it from postgresql.org if you don't have it.
3. Create a new Phoenix app
Generate a fresh app called inventory, fetch its dependencies, and create the
database:
mix phx.new inventory
cd inventory
mix ecto.create
Start it once to confirm a blank app runs:
mix phx.server
Visit http://localhost:4000 — you should see the Phoenix
welcome page. Stop the server with Ctrl+C twice.
What did that generate?
A Phoenix app is just a folder of Elixir code. The parts you'll touch in this tutorial:
lib/inventory/— your business logic (data, database access).lib/inventory_web/— your web layer (pages, routes, components).lib/inventory_web/router.ex— maps URLs to code.assets/css/app.cssandassets/js/app.js— your styles and JavaScript.mix.exs— your project's dependencies.
4. Add the data layer
This is the only step that differs by backend. Aurora UIX works with both plain Ecto schemas and Ash resources and detects which you're using automatically — so everything after this step is identical either way.
Pick one option below. Both create the same two tables with the same field
names, grouped under a context/domain called Inventory.Catalog with schemas
Inventory.Catalog.Category and Inventory.Catalog.Product. Because the module
names match, the next steps are byte-for-byte identical for both options.
New to all of this? Choose Option A. It has the least to learn. Building something closer to a production app? Option B (Ash) is what many real Aurora UIX apps use.
Why a context named Catalog, not Inventory?
Phoenix refuses to create a context with the same name as your app
(Inventory), so we group these schemas under Inventory.Catalog. This also
lines the Ecto path up perfectly with the Ash domain in Option B.
Option A — Phoenix + Ecto
Generate the two schemas (tables + Ecto schema modules). We use
phx.gen.schema — not phx.gen.context — because Aurora UIX gets its CRUD
functions from a tiny helper library, aurora_ctx,
in a moment:
mix phx.gen.schema Catalog.Category categories name description
mix phx.gen.schema Catalog.Product products \
name price:decimal stock:integer category_id:references:categories
The generator creates lib/inventory/catalog/category.ex and
lib/inventory/catalog/product.ex (plus migrations), but it does not wire up
the relationship. Open lib/inventory/catalog/product.ex and, inside the schema
block, replace the generated field :category_id, :id line with a real
association:
belongs_to :category, Inventory.Catalog.Category…and add :category_id to the changeset's cast/2 list so the form can set it:
|> cast(attrs, [:name, :price, :stock, :category_id])In lib/inventory/catalog/category.ex, add the other side of the relationship
inside its schema block:
has_many :products, Inventory.Catalog.ProductNow add aurora_ctx to your mix.exs dependencies — it generates the CRUD
functions (list_products/1, get_product/2, create_product/2, …) that Aurora
UIX expects:
{:aurora_ctx, "~> 0.1"}Run mix deps.get, then create the context module at lib/inventory/catalog.ex:
defmodule Inventory.Catalog do
use Aurora.Ctx
alias Inventory.Catalog.{Category, Product}
alias Inventory.Repo
ctx_register_schema(Category, Repo)
ctx_register_schema(Product, Repo)
endFinally, create the tables:
mix ecto.migrate
Why aurora_ctx?
Aurora UIX drives the UI through context functions like list_products/1. The
default mix phx.gen.context generates list_products/0 (no arguments), which
doesn't match. aurora_ctx's ctx_register_schema/2 generates the full set of
compatible functions for you in two lines. You can still add your own custom
functions to this module as your app grows.
That's it — skip ahead to "Either way" below.
Option B — Ash
Ash is a declarative resource framework. Install it with its igniter-powered installers, which scaffold the repo, config, and supervision tree for you:
mix igniter.install ash ash_postgres ash_phoenix
Then define a domain and two resources. Create lib/inventory/catalog.ex:
defmodule Inventory.Catalog do
use Ash.Domain
resources do
resource Inventory.Catalog.Category
resource Inventory.Catalog.Product
end
endCreate lib/inventory/catalog/category.ex:
defmodule Inventory.Catalog.Category do
use Ash.Resource,
domain: Inventory.Catalog,
data_layer: AshPostgres.DataLayer
postgres do
table "categories"
repo Inventory.Repo
end
attributes do
uuid_primary_key :id
attribute :name, :string, allow_nil?: false, public?: true
attribute :description, :string, public?: true
timestamps()
end
relationships do
has_many :products, Inventory.Catalog.Product, public?: true
end
actions do
defaults [:read, :destroy, create: :*, update: :*]
end
endWhy public?: true?
Ash attributes are private by default — they won't be accepted by the
create/update actions or rendered by the form unless you mark them
public?: true. Forgetting this is the most common reason an Ash form "saves
nothing."
Create lib/inventory/catalog/product.ex:
defmodule Inventory.Catalog.Product do
use Ash.Resource,
domain: Inventory.Catalog,
data_layer: AshPostgres.DataLayer
postgres do
table "products"
repo Inventory.Repo
end
attributes do
uuid_primary_key :id
attribute :name, :string, allow_nil?: false, public?: true
attribute :price, :decimal, public?: true
attribute :stock, :integer, public?: true
timestamps()
end
relationships do
belongs_to :category, Inventory.Catalog.Category,
public?: true,
attribute_writable?: true
end
actions do
defaults [:read, :destroy, create: :*, update: :*]
end
endattribute_writable?: true on the belongs_to lets the form set the category by
its id (category_id) — which is exactly what the dropdown you'll build in Step 6
does.
Generate and run the migrations for these resources:
mix ash_postgres.generate_migrations create_inventory
mix ecto.migrate
For this tutorial, use the domain (Inventory.Catalog) as the "context" and
the resources (Inventory.Catalog.Product, Inventory.Catalog.Category) in
the next step.
Using Ash policies?
We kept this resource open to stay simple. If your resources are protected by
Ash.Policy.Authorizer, add ash_actor_assign: :current_user to the metadata
so Aurora UIX threads the current user as the Ash actor through every generated
CRUD call. See
Ash Integration → Authorization & policies.
Either way — you're now on the shared path
One backend, almost-identical UI
From here on the steps are the same whether you chose Ecto or Ash — both options
gave you a context/domain Inventory.Catalog with Inventory.Catalog.Category
and Inventory.Catalog.Product. The only difference is one line in Step 6:
how each resource is declared (context:/schema: for Ecto vs ash_resource:
for Ash). Everything else — layouts, the category dropdown, routes, assets — is
identical, because Aurora UIX detects the backend automatically.
5. Add Aurora UIX
Add the dependency to mix.exs:
def deps do
[
# ...your other deps...
{:aurora_uix, "~> 0.1"}
]
endFetch it:
mix deps.get
Aurora UIX ships pre-built CSS themes, a set of icons, and a few JavaScript hooks. Wire them up once:
mix auix.gen.stylesheet
mix auix.gen.icons
In assets/css/app.css, add the imports at the end of the file — variables
first, then the rules last so Aurora UIX's component styles win. Use a
relative ./ path on each import (Tailwind v4 requires it):
/* Aurora UIX — add at the end of app.css */
@import "./auix-icons.css";
@import "./auix-variables.css"; /* Aurora UIX defaults */
@import "./auix-bridge-daisyui.css"; /* optional: follow your daisyUI theme */
@import "./auix-rules.css"; /* must be last */Already using Heroicons?
A fresh Phoenix 1.8 app already ships Heroicons. In that case you can skip
mix auix.gen.icons and instead add @source "../../deps/aurora_uix/priv/static/classes.js"; near the top of app.css so
Tailwind picks up only the icon classes Aurora UIX needs. Either approach works.
In assets/js/app.js, import Aurora UIX's hooks and spread them into your
LiveSocket:
import {Hooks as auroraHooks} from "../../deps/aurora_uix/assets/js/hooks.js"
const liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
hooks: {...auroraHooks}, // alongside your own hooks, if any
})Want the details — or your own theme?
The asset setup above is the short version. For the full explanation (including how to inherit your daisyUI theme automatically so Aurora UIX matches your brand with zero extra work), see Getting Started → CSS Configuration and the Writing a Style Bridge guide.
6. Describe the UI
Now the fun part. You describe each resource once with Aurora UIX metadata, and it
generates the index, show, and form views. We'll put both resources in a single
module — this matters: for the product form to render the category as a dropdown,
Aurora UIX needs the related resource (:category) registered alongside
:product in the same module.
Create lib/inventory_web/catalog_views.ex:
defmodule InventoryWeb.CatalogViews do
use Aurora.Uix
alias Inventory.Catalog
alias Inventory.Catalog.{Category, Product}
# Ecto (Option A): context: Catalog, schema: Category
# Ash (Option B): ash_resource: Category
auix_resource_metadata :category, context: Catalog, schema: Category do
field :name, placeholder: "Category name", required: true
field :description, max_length: 255
end
# Ecto (Option A): context: Catalog, schema: Product
# Ash (Option B): ash_resource: Product
auix_resource_metadata :product, context: Catalog, schema: Product do
field :name, placeholder: "Product name", required: true
field :price, precision: 12, scale: 2
field :stock, required: true
# Render the belongs_to as a dropdown, labelled by the category's name:
field :category_id, option_label: :name
end
auix_create_ui do
index_columns :category, [:name, :description]
edit_layout :category do
inline [:name, :description]
end
index_columns :product, [:name, :price, :stock]
edit_layout :product do
stacked do
inline [:name]
sections do
section "Pricing" do
stacked [:price]
end
section "Inventory" do
stacked [:stock, :category_id]
end
end
end
end
end
endTwo things to notice about the product's category picker:
- You reference the foreign-key field
:category_id(not:category) in both the metadata and the layout. Aurora UIX renders it as a<select>because it recognises thebelongs_to. field :category_id, option_label: :nametells it to label each option with the category'snameinstead of its raw id.
There's a lot more you can do here
This is a deliberately small slice. To learn every field option (validation, readonly, hidden fields, custom renderers, associations) see the Resource Metadata guide, and to build richer forms with tabs, nested sections, and one-to-many lists see the Layouts guide.
7. Add routes and run it
Open lib/inventory_web/router.ex and add the generated LiveViews to the browser
scope using Aurora UIX's route helper, which expands to the full set of CRUD
routes:
import Aurora.Uix.RouteHelper
scope "/", InventoryWeb do
pipe_through :browser
auix_live_resources "/categories", CatalogViews.Category
auix_live_resources "/products", CatalogViews.Product
endThe generated LiveView modules are named after the views module plus the
schema name — InventoryWeb.CatalogViews.Category and
InventoryWeb.CatalogViews.Product — which is exactly what the route helper
references here (the InventoryWeb part comes from the surrounding scope).
Start the server:
mix phx.server
Now visit:
- http://localhost:4000/categories — create a couple of categories first.
- http://localhost:4000/products — create a product and pick its category from the dropdown.
You have a complete, responsive CRUD app — list, detail, create, and edit views, with validation and real-time updates — and you wrote almost no UI code. 🎉
Generate routes for one resource at a time
auix_live_resources/2 accepts only: / except: to limit which routes it
creates (for example, a read-only screen). See
Getting Started → Add Routes to Router.
Where to go next
You now have a working app and the core workflow: describe a resource → lay out its views → add routes. From here, deepen each piece:
- Resource Metadata — every field option: validation, associations, readonly/hidden, and custom renderers.
- Layouts — the layout DSL you just used, in full: sections, tabs, nested forms, and one-to-many lists.
- Customizing & Extending — make the generated UI match your brand: styling, theming, and component overrides.
- LiveView Integration — hook your own event handlers and business logic into the generated views.
- Ash Integration — get the most out of Aurora UIX with Ash resources, including policies and actors.
- Internationalization — translate the generated UI.
- Troubleshooting — if something doesn't render, start here.