Treat Phoenix as the HTTP/realtime edge over OTP. Language, processes, and Mix: skill elixir. New code: Phoenix 1.8 — scopes for data access, LiveView when the UI is server-rendered, JSON controllers when the client is separate.
Stack and scope
| Ask | Role |
|---|---|
| Phoenix 1.8 | Endpoint, router, controllers, LiveView |
| Ecto | Schemas, changesets, migrations, Repo |
| Postgrex | Postgres (preferred) |
| PubSub | Fan-out / LiveView updates |
| Swoosh | E-mail (auth magic link included) |
| Bandit | HTTP server (recent default) |
Preserve what mix phx.new / the repo already generated (Tailwind, esbuild, core_components).
Organization
lib/
my_app/ # contexts (domínio)
accounts/
blog/
my_app_web/
router.ex
endpoint.ex
controllers/
live/
components/
plugs/
priv/repo/migrations/
test/
- Contexts (
MyApp.Blog) are the domain API:list_posts(scope),get_post!(scope, id). MyAppWebonly HTTP/LiveView: plug, params, render. No SQL in LiveView.router.exdeclares pipelines andlive_session; do not accumulate logic.
Non-negotiable rules
- Authn ≠ Authz. Scope (user, org, tenant) goes into every context function that reads/writes user data.
- Generators (
phx.gen.live,html,json) already thread scope through — do not remove the parameter "to simplify". - Changesets validate at the edge; never
Repo.insertwith a raw params map. - Migrations: never edit a migration already applied in production — create another.
- Do not expose Ecto schema with internal fields (password hash, tokens) in JSON/LiveView — use structs/JSON views.
- Secrets via runtime config / env (
config/runtime.exs), not hardcoded. force_ssland secure cookies in prod.
Scopes (1.8)
The scope centralizes the request context (current_user, org, …) and filters queries/PubSub.
def list_posts(%Scope{} = scope) do
Post
|> where(user_id: ^scope.user.id)
|> Repo.all()
end
def get_post!(%Scope{} = scope, id) do
get_post_query(scope)
|> Repo.get!(id)
end
- LiveView: authenticated routes inside
live_sessionwithon_mountthat definescurrent_scope. - PubSub: already scoped topics (
org:#{id}) to avoid leaking cross-tenant events. mix phx.gen.authdefines the default scope and magic link — do not switch to password just "because it was always like that" without a request.
Router and pipelines
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", MyAppWeb do
pipe_through [:browser, :require_authenticated_user]
live_session :authenticated,
on_mount: [{MyAppWeb.UserAuth, :ensure_authenticated}] do
live "/posts", PostLive.Index, :index
end
end
- Browser vs API in separate pipelines.
- Auth plugs in the pipeline; fine-grained authorization in the context (scope).
LiveView
mount/handle_paramsto load data via context + scope.- Events:
handle_eventlean → context →assign/ stream. - Streams for lists; avoid sending giant collections over the socket.
live_redirect/ patches for navigation; CSRF is already in the Phoenix form.- JS hooks only for browser APIs (clipboard, map); not for business rules.
Ecto
- Schema ≠ public API. Changeset per action (
create_changeset,update_changeset). Ecto.Multi/Repo.transactionfor compound writes.- Avoid N+1:
preloadconscious orjoin. - Database constraints (unique, FK) + changeset (
unique_constraint). - Soft delete only with consistent filtering in all queries of the context.
JSON / controllers
action_fallbackand stable views/JSON{data | errors}.- Params:
params["id"]+ changeset; strong-ish via embedded schema or changeset. - Correct HTTP statuses (
404,401,403,422). Contracts: skillapi-design.
Tests
ConnCase/LiveViewTest/DataCase.- Cover: happy path, 401/403, invalid changeset, scope (user A does not read user B's resource).
- Ecto sandbox; do not share a real DB between async tests without setup.
Anti-patterns
Repo.getwithout scope on a user resource- SQL/Ecto in the
handle_event - Migration rewritten after deploy
- LiveView for a mobile API (prefer JSON)
- Subscribe to global PubSub and filter "on the client"
String.to_atomin params
Integration with other skills
- Language/OTP:
elixir - HTTP contracts:
api-design - Deep authz:
authentication-authorization - Container:
docker - Go-live:
production-readiness
Completion criteria
- Contexts receive scope; queries filtered
- LiveView or JSON aligned with the real client
- Changesets + new migration (not edited) if the schema changed
- Authn in the pipeline; authz in the domain
- Ownership/403 tests passing
- Config via env; SSL/cookies ok for the target environment