GuidesElixir
Phoenix LiveView with Ecto and PostgreSQL
Create a Phoenix LiveView app with Ecto SQL, PostgreSQL, LiveView streams, ExUnit, Credo, and a release Dockerfile using Better Fullstack.
Use this stack when the server should own page state and rendering, and the data lives in PostgreSQL through Ecto.
npm create better-fullstack@latest my-liveview-app -- \ --ecosystem elixir \ --elixir-web-framework phoenix-live-view \ --elixir-orm ecto-sql \ --elixir-auth none \ --elixir-api none \ --elixir-realtime live-view-streams \ --elixir-jobs none \ --elixir-validation ecto-changesets \ --elixir-testing ex_unit \ --elixir-quality credo \ --elixir-deploy dockerWhat this creates
- A Phoenix 1.7 application with
phoenix_live_viewand the LiveView socket mounted at/live. - Ecto SQL with Postgrex, a
Repo, and anitemsmigration. - An
ItemLive.IndexLiveView at/itemsthat renders items through LiveView streams and creates items from aphx-submitform. - A root layout that loads the Phoenix and LiveView JavaScript clients from the installed Hex packages and connects a
LiveSocket. - A JSON health endpoint at
/api/health. - ExUnit with a
ConnCasesupport module, Credo, and a multi-stageDockerfilethat builds a Mix release.
Generated shape
Module names come from the project name: my-liveview-app becomes the :my_liveview_app OTP app, MyLiveviewApp, and MyLiveviewAppWeb.
my-liveview-app/
├── mix.exs
├── Dockerfile
├── .env.example
├── config/
│ ├── config.exs
│ ├── dev.exs
│ ├── prod.exs
│ ├── runtime.exs
│ └── test.exs
├── lib/
│ ├── my_liveview_app/
│ │ ├── application.ex
│ │ ├── catalog.ex
│ │ ├── catalog/item.ex
│ │ └── repo.ex
│ └── my_liveview_app_web/
│ ├── endpoint.ex
│ ├── router.ex
│ ├── components/layouts/root.html.heex
│ ├── controllers/
│ └── live/item_live/index.ex
├── priv/repo/
│ ├── migrations/20260101000000_create_items.exs
│ └── seeds.exs
└── test/
├── support/conn_case.ex
└── my_liveview_app_web/controllers/health_controller_test.exsPrerequisites
- Elixir 1.16 or newer with a matching Erlang/OTP release. The generated
Dockerfilebuilds on Elixir 1.16.3 and OTP 26. - A local PostgreSQL server.
config/dev.exsreadsPOSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_HOST, andPOSTGRES_DB, withpostgres/postgresonlocalhostas defaults.
Run and verify
cd my-liveview-app
mix deps.get
mix ecto.setup
mix phx.servermix ecto.setup is a generated alias that creates the database, runs migrations, and runs priv/repo/seeds.exs, which inserts one sample item. The server listens on port 4000 unless PORT is set. GET /api/health returns {"status":"ok"} with the app name, and LiveDashboard is available at /dev/dashboard in development.
mix testThe test alias creates and migrates the test database before running ExUnit, so PostgreSQL must be reachable for the test run too.
Use the form on /items
Open http://localhost:4000/items. The page lists the seed item, and the browser connects to the LiveView socket at /live. Submitting the form with a name sends the create event to handle_event/3, which inserts the item and appends it to the list without a page reload. A name shorter than two characters keeps the view connected and shows Name should be at least 2 character(s) under the field, with the typed values still in the inputs.
The scaffold has no assets/ directory or JavaScript build step. The endpoint serves phoenix.min.js and phoenix_live_view.min.js straight from the phoenix and phoenix_live_view packages, and an inline script in components/layouts/root.html.heex starts the LiveSocket. Add an asset pipeline, such as the esbuild Hex package, when you need your own hooks or bundled JavaScript.
Representative snippets
The LiveView mounts with a stream and inserts new rows without re-rendering the list:
def mount(_params, _session, socket) do
socket =
socket
|> assign(:form, empty_form())
|> stream(:items, Catalog.list_items())
{:ok, socket}
end
def handle_event("create", %{"item" => item_params}, socket) do
case Catalog.create_item(item_params) do
{:ok, item} ->
{:noreply, socket |> stream_insert(:items, item) |> assign(:form, empty_form())}
{:error, changeset} ->
{:noreply, assign(socket, :form, to_form(changeset, as: "item"))}
end
endto_form/2 accepts the changeset because the scaffold depends on phoenix_ecto. The template renders each message from @form[:name].errors below the name input.
Validation lives in the Ecto changeset, so the same rules apply to every caller of Catalog.create_item/1:
def changeset(item, attrs) do
item
|> cast(attrs, [:name, :description])
|> validate_required([:name])
|> validate_length(:name, min: 2, max: 120)
endWhen to choose it
Choose LiveView when most of the UI is forms, lists, and dashboards that benefit from server-rendered state, and you would rather not maintain a separate JavaScript frontend and API contract.
Choose the Phoenix GraphQL guide or the Phoenix and Oban guide when another client consumes the backend over HTTP.
Compatibility notes
- With
--elixir-web-framework phoenix-live-viewand--elixir-api none, the realtime choice islive-view-streams. --elixir-jobs obanrequires--elixir-orm ecto-sql; the CLI rejects Oban with any other database layer.- The Elixir database comes from
--elixir-orm.ecto-sqluses Postgrex and PostgreSQL;myxqlandecto_sqlite3select MySQL and SQLite instead.
Deployment notes
The Dockerfile runs mix release with MIX_ENV=prod and starts the result with bin/my_liveview_app start. Run the same build locally with:
MIX_ENV=prod mix deps.get --only prod
MIX_ENV=prod mix releaseconfig/runtime.exs sets the production values when the release boots:
| Variable | Required | Purpose |
|---|---|---|
SECRET_KEY_BASE | Yes | Signs sessions and LiveView tokens. The release stops at boot without it. |
DATABASE_URL | Yes | The only production database setting. Without it, every query fails. |
PHX_HOST | No | Host used in generated URLs. Defaults to example.com. |
PORT | No | HTTP port. Defaults to 4000. |
POOL_SIZE | No | Ecto pool size. Defaults to 10. |
ECTO_SSL | No | Set to true to connect to PostgreSQL over SSL. |
The scaffold does not generate a release migration module, so mix ecto.migrate is not available inside the release. Add a Release.migrate/0 function, or run migrations from a build step that has Mix, before routing traffic.
Troubleshooting
| Symptom | Check |
|---|---|
Name should be at least 2 character(s) under the form | The item changeset requires a name of 2 to 120 characters. The view stays connected and keeps the typed values; correct the name and submit again. |
mix ecto.setup cannot connect | Confirm PostgreSQL is running and the POSTGRES_* variables match your server. |
Release exits with SECRET_KEY_BASE is missing | Generate one with mix phx.gen.secret and set it in the runtime environment. |
Release queries fail with missing the :database key | Set DATABASE_URL; production has no other database configuration. |
relation "items" does not exist in production | Migrations do not run at release start; apply them before deploying. |
Next steps
- Open the Stack Builder.
- Read the Elixir ecosystem docs.
- Review the CLI create reference.