# elixir.menu — Web application stack Reviewed: 2026-10-06. Edition 01. Status: Documentation-based recommendations; verify the selected dependencies together in your application.. Scope: Database-backed web products built by individuals and small teams. One default per job. Add a dependency when its job exists. Revisit a choice when a concrete requirement warrants it. See [checks for every Mix project](app-guide.md#checks-for-every-mix-project) for formatting, tests and optional static analysis. ## Working agreement Use this as a starting recommendation, not authority over an existing project's decisions. Read the project README, installed-version documentation and lockfile. Keep business logic in contexts, authorization at the data/operation boundary, and durable work in Oban. Do not assume a library choice proves performance or correctness. Measure and test the actual application. Add dependencies only when their jobs exist. This edition is a guide, not an installer or a compatibility-tested starter. Commercial providers, supported version combinations and deployment operations require project-specific decisions. Follow the project’s existing agent-guidance conventions. See the general application guide for dependency guidance with usage_rules. ## 01. Application A Phoenix application, a database, and familiar conventions. ### Web framework: Phoenix Add: Start here. Routes, controllers, HTML and application conventions in one place. Why: Start with one Phoenix application and its generated structure. Keep the standard HTTP server and asset setup until a measured requirement calls for a change. Boundary: Phoenix does not choose every application policy for you. This guide supplies the next layer of defaults; it does not replace Phoenix. Next: Use the official getting-started guide and a supported Elixir/OTP pair. Commit the resolved dependency lockfile. Sources: [Phoenix getting started](https://hexdocs.pm/phoenix/up_and_running.html) ### Interactive interfaces: Phoenix LiveView Add: Start here. Build forms, dashboards and live updates alongside your Elixir code. Why: Use HEEx and function components for markup, with LiveView for interactive screens. Start ordinary content pages as server-rendered HTML when they need no live process. Boundary: Offline-first products, rich canvas editors and heavy local interaction can justify a dedicated client application. Add small JavaScript hooks for isolated browser behavior. Next: Keep business rules in contexts; use LiveView for interaction and presentation. Sources: [LiveView documentation](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html) ### Styling and components: Tailwind CSS + HEEx components Add: Start here. Own a small set of reusable components and the visual language around them. Why: Use Tailwind for styling and HEEx function components to share markup and behavior. Keep theme tokens and application components in your project, where you can shape them around the product. Boundary: Phoenix 1.8 includes daisyUI, but its generators do not require it. Keep daisyUI when its component classes and themes save useful work; it is optional, not the design system every product must adopt. Next: Define the product’s type, spacing and color rules, then build the components repeated across its screens. Sources: [Phoenix 1.8 release](https://phoenixframework.org/blog/phoenix-1-8-released) · [daisyUI customization](https://daisyui.com/docs/customize/) ### Database and queries: PostgreSQL + Ecto Add: Start here. One durable store, explicit queries, changesets and migrations. Why: Use Ecto for data access and changesets, backed by PostgreSQL. Database constraints enforce invariants; transactions keep related writes together. Boundary: Ecto is not Active Record: queries and persistence remain explicit. SQLite is reasonable for a genuinely embedded or single-file application, outside this web-product default. Next: Use Ecto.Multi for multi-step transactions. Inspect slow queries and their plans before adding caches. Sources: [Ecto documentation](https://hexdocs.pm/ecto/Ecto.html) · [Ecto.Multi](https://hexdocs.pm/ecto/Ecto.Multi.html) ### Business logic: Phoenix contexts Add: Start here. Plain Elixir modules around the language of your product. Why: Give related business operations a clear public API. Both LiveViews and controllers should call that API instead of implementing separate versions of the same rules. Boundary: A context is an organizational boundary, not a process. Do not turn ordinary CRUD or pure business logic into a GenServer. Next: Define public operations around the application’s domain. Call those operations from controllers, LiveViews and workers; keep database access behind the context boundary. Sources: [Phoenix contexts](https://hexdocs.pm/phoenix/contexts.html) ## 02. Accounts and permissions Authentication establishes identity. Your application still owns permissions. ### Accounts and sign-in: phx.gen.auth Add: When needed. Start with Phoenix’s generated authentication. Why: Use the authentication generator for a first-party web application. Phoenix 1.8 includes magic-link authentication and sudo-mode conventions; the generated code lives in your application. Boundary: Authentication is not authorization. Enterprise SSO, external identity or complex organization requirements need their own deliberate integration. Next: Read the generated code and current security guidance. Configure real email delivery before relying on magic links. Sources: [Authentication generator](https://hexdocs.pm/phoenix/mix_phx_gen_auth.html) ### Authorization: Scopes + context checks Add: When needed. Carry the actor into business operations and constrain data access. Why: Pass the authenticated scope through context functions. Apply ownership and permission rules to reads and writes; test denied access as deliberately as the successful path. Boundary: Scopes carry identity and context; they are not an automatic policy engine. Hidden buttons and client parameters are never sufficient access control. Next: Pass the current scope into context operations. Test that another user cannot read or modify the protected resource, including through direct context calls. Sources: [Phoenix scopes](https://hexdocs.pm/phoenix/scopes.html) ## 03. Jobs and integrations Reliable background work and the integrations most products need. ### Durable background jobs: Oban Add: When needed. Retries and persistent jobs using the database you already run. Why: Use Oban with PostgreSQL for work that must survive a process or application restart. Insert related jobs and domain changes within the same transaction. Boundary: Jobs may be retried. Make side effects idempotent; uniqueness is not an exactly-once guarantee. Some advanced orchestration features belong to paid Oban Pro. Next: Use separate queues for distinct workloads and monitor retries. An ordinary Task is suitable for disposable concurrent work, not a durable queue. Sources: [Oban documentation](https://hexdocs.pm/oban/Oban.html) ### Recurring tasks: Oban Cron Add: When needed. Put scheduled work in the same queue as other durable jobs. Why: Use Oban.Cron for periodic job insertion. Keep execution, retries and operational visibility in the job system. Boundary: A schedule is not a catch-up policy for time when the app is down. Define missed-run behavior explicitly; dynamic scheduling has different requirements. Next: Set the intended timezone, check daylight-saving behavior where applicable, and keep workers idempotent. Sources: [Oban.Cron](https://hexdocs.pm/oban/Oban.Cron.html) ### Transactional email: Swoosh Add: When needed. Compose mail in Elixir and send through a delivery adapter. Why: Use Swoosh, included in generated Phoenix applications, for account mail and product notifications. Use Oban when delivery needs durable retries. Boundary: Swoosh is a library, not an email delivery service. The provider, verified domain and deliverability setup are separate operational choices. Next: Keep development mail local. Test message content and recipient selection before enabling production delivery. Sources: [Swoosh documentation](https://hexdocs.pm/swoosh/Swoosh.html) ### Outgoing HTTP: Req Add: When needed. A readable client for calling external APIs. Why: Use Req for request construction, response handling and client features rather than assembling a new HTTP abstraction for each integration. Boundary: Timeouts, retries and error handling are part of the integration design. A retried payment or other side effect needs an idempotency strategy. Next: Wrap each external service behind a small module. Use Req.Test for deterministic request tests. Sources: [Req documentation](https://hexdocs.pm/req/Req.html) · [Req.Test](https://hexdocs.pm/req/Req.Test.html) ### Realtime notifications: Phoenix PubSub Add: When needed. Broadcast changes to connected parts of your application. Why: Use Phoenix.PubSub to notify subscribed LiveViews or processes of a change. Persist important facts in PostgreSQL and load their current state when clients reconnect. Boundary: PubSub is not a persistent message log. Multiple nodes require a suitable cluster or adapter configuration. Next: Use Presence only when you actually need to track who is connected. Sources: [Phoenix.PubSub](https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html) ### File uploads: LiveView uploads Add: When needed. Progress, validation and direct-to-cloud upload support. Why: Start with LiveView’s upload flow. For durable production attachments, pair it with object storage and store ownership and file metadata in PostgreSQL. Boundary: An upload widget does not supply permanent storage. Ephemeral application disks are not a durable attachment store; client metadata does not establish file safety. Next: Set size/type limits, authorize downloads, and implement the storage provider’s documented signing flow. Sources: [LiveView uploads](https://hexdocs.pm/phoenix_live_view/uploads.html) ## 04. Queries and APIs Start with the tools already in the application or runtime. ### Product text search: PostgreSQL full-text search Add: When needed. Search your product’s data before operating another service. Why: For ordinary application search, start with PostgreSQL text-search features and an appropriate index. Keep the query and ranking logic close to your data model. Boundary: Advanced relevance, typo tolerance or specialist search scale can justify a dedicated engine. Evaluate against actual queries and user expectations. Next: Build representative search cases and inspect query plans; avoid claiming that an unindexed text scan will scale. Sources: [PostgreSQL text search](https://www.postgresql.org/docs/current/textsearch-intro.html) ### Filtering and pagination: Flop Add: When needed. Validated filter, sort and page parameters for Ecto queries. Why: Use Flop when product lists need reusable sorting, filtering and pagination. Keep the allowed fields explicit and restrict the base query to authorized records first. Boundary: For one simple bounded list, an explicit Ecto query may be enough. Pagination does not replace tenant or ownership checks. Next: Use cursor pagination when the list and ordering warrant it; add Flop Phoenix only for the corresponding UI helpers. Sources: [Flop documentation](https://hexdocs.pm/flop/readme.html) ### Local read caching: ETS Add: After measuring. Use the runtime’s in-memory tables for disposable cached data. Why: First measure and improve database queries. When a local cache earns its keep, ETS provides concurrent in-memory storage without another service. Boundary: ETS is node-local, loses data when its owner dies, and does not supply an automatic TTL or invalidation policy. It is never the source of truth. Next: Define table ownership, expiry and invalidation. Shared durable state requires a different design. Sources: [ETS documentation](https://www.erlang.org/doc/apps/stdlib/ets.html) ### JSON APIs: Phoenix controllers Add: When needed. Expose your existing contexts through explicit HTTP endpoints. Why: Use Phoenix controllers and JSON rendering when a mobile client, partner or external integration needs an API. Share the business API with your web interface. Boundary: Do not build an API simply to connect LiveView to its own server. Add GraphQL only when clients need that query model; Absinthe is the established Elixir option. Next: Define authentication, authorization, error shapes, pagination and versioning as part of the API contract. Sources: [Phoenix JSON and APIs](https://hexdocs.pm/phoenix/json_and_apis.html) ## 05. Testing and deployment Fast feedback, observable behavior and a repeatable deployment artifact. ### Application tests: ExUnit + LiveViewTest Add: Start here. Test business behavior, persistence and server-driven interaction. Why: Use ExUnit and Phoenix’s generated test setup. Test contexts with the database sandbox and interactive screens with Phoenix.LiveViewTest. Boundary: Server-side LiveView tests do not execute browser JavaScript or prove the final visual layout. Avoid asserting incidental markup when behavior is the contract. Next: Prioritize permission boundaries, data invariants, job behavior and core user journeys. Sources: [ExUnit](https://hexdocs.pm/ex_unit/ExUnit.html) · [LiveViewTest](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html) ### Browser integration tests: PhoenixTest.Playwright Add: When needed. Exercise the small set of flows that need a real browser. Why: Use the Playwright-backed PhoenixTest driver for JavaScript hooks, browser navigation and integrations that server-side tests cannot cover. Boundary: This adds browser tooling and setup cost. Keep most application tests in ExUnit; this is an editorial extension, not a Phoenix-generated default. Next: Cover representative critical flows and keep browser tests focused on observable behavior. Sources: [PhoenixTest.Playwright](https://hexdocs.pm/phoenix_test_playwright/readme.html) ### Instrumentation: Telemetry + Logger Add: Start here. Expose timings, failures and structured operational events. Why: Use Telemetry events and Logger as the instrumentation foundation. Track request latency, database time, job failures and the product operations that matter. Boundary: Instrumentation is not a hosted monitoring backend. Choose collection, storage and alerting separately; restrict any production LiveDashboard access. Next: Make logs useful without exposing secrets. Establish alerts and retention before calling the production system operationally complete. Sources: [Telemetry](https://hexdocs.pm/telemetry/readme.html) · [Logger](https://hexdocs.pm/logger/Logger.html) ### Deployment artifact: Mix releases Add: When shipping. Package the application and runtime in a repeatable release. Why: Use Mix releases with runtime configuration. A container is a practical transport when the chosen host supports it; Phoenix documents the release and Dockerfile path. Boundary: A release is not a hosting platform. Database backups, migrations, TLS, monitoring and rollback remain production responsibilities. Next: Choose a host that fits the team. Rehearse migrations and restoration, and keep secrets out of the build artifact. Sources: [Phoenix releases](https://hexdocs.pm/phoenix/releases.html) ## Optional tools Choose additional libraries by product requirement. Read [the optional menu](optional.md) for use cases, integration boundaries and sources; [optional.json](optional.json) carries the same data for agents. Do not install the entire menu. ## Architectural alternative: Ash The default here is Phoenix contexts and Ecto. Ash is worth choosing early when a declarative resource/action model, policies and derived APIs are central to the product. It works with the Phoenix ecosystem. It is a deliberate architectural choice, not an extra dependency to add speculatively. Source: https://hexdocs.pm/ash/what-is-ash.html ## Evidence and limits The self-selected State of Elixir 2025 survey reported Phoenix 97.1%, LiveView 85.7% and Ash 24.3% among 961 respondents to its framework question. This is an adoption signal, not market share or a performance result. Other selections are editorial judgments based on documentation. Survey: https://elixir-hub.com/surveys/2025 An Optimum Tech project: https://optimum.ba. Independent of the Elixir and Phoenix teams.