A production-ready Phoenix + Inertia.js (React, SSR) starter — authentication, real-time, i18n, theming, testing, CI, and Docker, already wired and tested.
I want to build web apps using Phoenix. LiveView is great, but I'm familiar with and prefer the rich React ecosystem. In the past, I would have reached for a frontend framework (e.g. Next.js), with an Elixir backend API. This has numerous downsides, including having to manage and sync deployments, migrations, versions, additional infrastructure etc. all of which compound complexity exponentially. Inertia.js provides a way of acting as the glue between Phoenix and React (or Vue or Svelte), so you don't have to deal with the complexities of separate repos.
This template project serves as a starting point for a lot of my web applications, so that I don't have to duplicate a lot of the setup work. It's fully featured (see Features below). Feel free to use it however you want or submit a PR if you find some way of improving it.
It's provided as-is, with no guarantees or warranties of any kind — use it at your own risk.
- Authentication — email + password with link-based confirmation and password reset, Argon2 hashing, sliding sessions, account management, and no email enumeration.
- Real-time — token-authenticated Phoenix Channels with React hooks that survive Inertia navigation.
- Internationalization — gettext on the server and react-i18next on the client; English + Spanish with locale detection.
- Theming — light / dark / system with no flash of the wrong theme.
- Accessibility — Radix UI primitives, Biome a11y linting in CI, and axe-core auditing in dev.
- Email — Swoosh (HTML + text) via Mailjet in production, with an in-browser mailbox in dev.
- Security — rate-limited auth endpoints, Secure cookies, CSRF protection, and HTTPS/HSTS in production.
- Background jobs — Oban, configured and ready.
- Testing — ExUnit with a coverage gate, ex_machina factories, and a Playwright E2E suite.
- Tooling & CI — mise-pinned toolchain, Biome / Credo / Dialyzer, and GitHub Actions.
- Deployment — multi-stage Docker release with server-side rendering.
- Documentation — ex_doc guides and module reference.
- Backend — Elixir · Phoenix (Bandit) · Ecto + PostgreSQL · Oban · Swoosh · gettext
- Frontend — Inertia.js · React (SSR) · Tailwind CSS · Radix UI · esbuild · TypeScript · Biome
- Tooling — mise · Credo · Dialyzer · ex_doc · ExUnit + ex_machina · Playwright · GitHub Actions · Docker
Prerequisites: mise (pins the toolchain) and a running PostgreSQL.
Starting a new project from this template? Rename it to your own name first:
scripts/rename_project.sh YourAppName # PascalCaseIt rewrites every module, the OTP app name, file/directory paths, and configs (both
PascalCaseandsnake_caseforms), then rebuilds and runsmix precommitto verify. Use--dry-runto preview the changes first.
# 1. Install the pinned Erlang, Elixir, and Node (versions live in mise.toml)
mise trust && mise install
# 2. Install deps, create + migrate the database, build assets
mix setup
# 3. Start the server
mix phx.server # or: iex -S mix phx.serverVisit localhost:4000. With shell activation enabled
(eval "$(mise activate zsh)" in ~/.zshrc), mise switches to the project's
toolchain automatically when you cd in.
mix test # unit + integration, with coverage gate (mix test --cover)
mix precommit # format, credo, biome, i18n, tests — run before every commit
npm --prefix assets run e2e # Playwright E2E (needs a running server; see the guide)See the End-to-End Testing guide for the Playwright setup.
Searchable developer guides and the full module reference are generated with ex_doc:
mix docsIn development they're also served at
/dev/docs. Topic guides live in
docs/ — e.g. the End-to-End Testing guide.
SECRET_KEY_BASE must be generated once and kept stable across restarts — a
changing value invalidates every session and signed cookie. Generate it a single
time with mix phx.gen.secret and store it in your secrets manager or host config,
then build and run the release image (reading the stored values from the environment):
docker build -t elixir_react_starter .
docker run -p 4000:4000 \
-e DATABASE_URL="ecto://USER:PASS@HOST/DB" \
-e MAILJET_API_KEY="$MAILJET_API_KEY" \
-e MAILJET_SECRET="$MAILJET_SECRET" \
-e PHX_HOST="example.com" \
-e SECRET_KEY_BASE="$SECRET_KEY_BASE" \
elixir_react_starterconfig/runtime.exs requires DATABASE_URL, MAILJET_API_KEY, MAILJET_SECRET,
and SECRET_KEY_BASE and will refuse to boot without them. PHX_HOST defaults to
example.com but should always be set in production (it's used for absolute URLs
in emails). PORT (default 4000), POOL_SIZE, ECTO_IPV6, and
DNS_CLUSTER_QUERY are optional.
Database migrations run via the release: bin/migrate. See Phoenix's
deployment guides for hosting specifics.
GET /health returns {"status":"ok"} — cheap, no database calls, no external
lookups. The image's HEALTHCHECK polls it via the embedded Node binary, and
it's a sensible target for Kubernetes liveness/readiness probes too.
Runtime versions are pinned in two places that must agree: mise.toml (dev
and CI toolchain, via mise) and the ARG declarations
at the top of the Dockerfile (release build). mix precommit runs
scripts/check-tool-versions.sh as its first step and will fail if those drift —
so when bumping a runtime version, update both files in the same commit.
Project architecture, invariants, and coding conventions are documented in
CLAUDE.md — start there before making changes.
Released under the WTFPL (see the LICENSE file) — do what the fuck you want.
