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.

Updated October 6, 2026phoenixliveviewectopostgres

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 docker

What this creates

  • A Phoenix 1.7 application with phoenix_live_view and the LiveView socket mounted at /live.
  • Ecto SQL with Postgrex, a Repo, and an items migration.
  • An ItemLive.Index LiveView at /items that renders items through LiveView streams and creates items from a phx-submit form.
  • 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 ConnCase support module, Credo, and a multi-stage Dockerfile that 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.

txt
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.exs

Prerequisites

  • Elixir 1.16 or newer with a matching Erlang/OTP release. The generated Dockerfile builds on Elixir 1.16.3 and OTP 26.
  • A local PostgreSQL server. config/dev.exs reads POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST, and POSTGRES_DB, with postgres/postgres on localhost as defaults.

Run and verify

bash
cd my-liveview-app
mix deps.get
mix ecto.setup
mix phx.server

mix 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.

bash
mix test

The 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:

elixir
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
end

to_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:

elixir
def changeset(item, attrs) do
  item
  |> cast(attrs, [:name, :description])
  |> validate_required([:name])
  |> validate_length(:name, min: 2, max: 120)
end

When 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-view and --elixir-api none, the realtime choice is live-view-streams.
  • --elixir-jobs oban requires --elixir-orm ecto-sql; the CLI rejects Oban with any other database layer.
  • The Elixir database comes from --elixir-orm. ecto-sql uses Postgrex and PostgreSQL; myxql and ecto_sqlite3 select 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:

bash
MIX_ENV=prod mix deps.get --only prod
MIX_ENV=prod mix release

config/runtime.exs sets the production values when the release boots:

VariableRequiredPurpose
SECRET_KEY_BASEYesSigns sessions and LiveView tokens. The release stops at boot without it.
DATABASE_URLYesThe only production database setting. Without it, every query fails.
PHX_HOSTNoHost used in generated URLs. Defaults to example.com.
PORTNoHTTP port. Defaults to 4000.
POOL_SIZENoEcto pool size. Defaults to 10.
ECTO_SSLNoSet 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

SymptomCheck
Name should be at least 2 character(s) under the formThe 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 connectConfirm PostgreSQL is running and the POSTGRES_* variables match your server.
Release exits with SECRET_KEY_BASE is missingGenerate one with mix phx.gen.secret and set it in the runtime environment.
Release queries fail with missing the :database keySet DATABASE_URL; production has no other database configuration.
relation "items" does not exist in productionMigrations do not run at release start; apply them before deploying.

Next steps

GitHub Sponsors