Skip to content
Development
Skill

/phoenix-scopes-and-generators

Use when running phx.gen.live/html/json/context with scopes (--scope, --no-scope), editing config :my_app, :scopes, extending the %Scope{} struct with an org, or adopting scopes in a pre-1.8 app. Not for login plumbing (phoenix-liveview-auth) or permission rules

BOOST
From plugin
elixir-phoenix-guide
16924 skills3 hooks
Install
$ npx -y skills add j-morgan6/elixir-phoenix-guide --skill phoenix-scopes-and-generators --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/phoenix-scopes-and-generators

Context preview

The summary Claude sees to decide when to auto-load this skill.

Use when running phx.gen.live/html/json/context with scopes (--scope, --no-scope), editing config :my_app, :scopes, extending the %Scope{} struct with an org, or adopting scopes in a pre-1.8 app. Not for login plumbing (phoenix-liveview-auth) or permission rules

SKILL.md

phoenix-scopes-and-generators.SKILL.md
name: phoenix-scopes-and-generators
description: "Use when running phx.gen.live/html/json/context with scopes (--scope, --no-scope), editing config :my_app, :scopes, extending the %Scope{} struct with an org, or adopting scopes in a pre-1.8 app. Not for login plumbing (phoenix-liveview-auth) or permission rules (phoenix-authorization-patterns)."

Phoenix Scopes and Generators

> Verified against Elixir 1.20, Phoenix 1.8, LiveView 1.2, Ecto 3.14 — October 2026

RULES — Follow these with no exceptions

1. **Check `config :my_app, :scopes` before generating.** Generators read it at mix-task time; with no default entry (no `phx.gen.auth` yet) they silently emit unscoped contexts with no PubSub topic and no owner column. Run `phx.gen.auth` first, or add the entry. 2. **Take the default scope unless the data is truly global.** `--no-scope` (lookup tables, public content) drops the scope argument, the owner column, the `subscribe_*/broadcast_*` functions and the cross-scope tests. `--scope name` only accepts a configured entry, and combining the two flags raises. 3. **Don't pass the owner as a field.** With the default scope, `user_id:references:users` aborts the generator ("same name as the scope schema key"); the scope already adds `field :user_id` and the migration column. 4. **Keep the generated ownership checks.** `update_*`, `delete_*` and `change_*` all start with `true = post.user_id == scope.user.id`. A hand-written "new" form must build `%Post{user_id: scope.user.id}`, or `change_post/2` raises `MatchError`. 5. **Extend the one `Scope` struct; don't add a second assign.** Add fields (`org`, `org_role`) plus a builder (`put_org/3`), and a second config entry that points at the **same** `module` and `assign_key` with a different `access_path` and `schema_key`. 6. **Fill extra scope fields with a plug *and* an `on_mount` hook.** The plug covers the HTTP render and controllers; the hook covers the websocket mount and live navigation. Both look up membership through the current scope and never trust the slug alone. 7. **Write the test helpers before generating against a new entry.** Generated tests call `<name>_scope_fixture/0` from `test_data_fixture` and `setup :<test_setup_helper>`. Both must exist, and the setup helper must return `scope:`. 8. **Scoped controllers need a pipeline that assigns the scope.** `phx.gen.json` output reads `conn.assigns.current_scope`, but the generated `:api` pipeline never sets it, so every action fails with `KeyError`. 9. **Pass the scope down; never rebuild it from params.** Contexts pass it to other contexts. Background jobs store ids and rebuild it in `perform/1`. 10. **Adopt scopes in a pre-1.8 app in this order:** Scope module, then config, then assign `:current_scope`, then test helpers, then owner columns, then generate or convert contexts one at a time.

---

The Two Pieces `phx.gen.auth` Adds

`lib/my_app/accounts/scope.ex` is a plain struct with `for_user/1` (`nil` in gives `nil` out; guests have no scope). Its moduledoc asks you to extend it. The config entry tells every later generator how to use it:

config :my_app, :scopes,
  user: [
    default: true,
    module: MyApp.Accounts.Scope,
    assign_key: :current_scope,
    access_path: [:user, :id],
    schema_key: :user_id,
    schema_type: :id,
    schema_table: :users,
    test_data_fixture: MyApp.AccountsFixtures,
    test_setup_helper: :register_and_log_in_user
  ]

Keys accepted by `Mix.Phoenix.Scope` (an unknown key raises):

| Key | Drives | |---|---| | `default` | used when no flag is given; only one entry may be `true` (more raises) | | `module` | the struct pattern-matched in context heads (`%Scope{} = scope`) | | `assign_key` | `socket.assigns.current_scope` / `conn.assigns.current_scope` in generated web code | | `access_path` | the owner value: `[:user, :id]` becomes `scope.user.id` in queries, `put_change` and topics | | `schema_key` | the owner field added to the schema and migration (`:user_id`) | | `schema_type` | that field's type (`:id`, `:binary_id`) | | `schema_table` | `references(:users, on_delete: :delete_all)`; `nil` gives a plain column with no FK | | `schema_migration_type` | overrides the migration column type (defaults to `schema_type` unless it is `:id`) | | `route_prefix` | e.g. `"/orgs/:org_slug"`, prepended to every generated path; at most one `:param` | | `route_access_path` | what fills that param, e.g. `[:org, :slug]` (defaults to `access_path` minus its last element) | | `test_data_fixture` | the module generated tests import `<name>_scope_fixture/0` from | | `test_setup_helper` | the `setup :helper` in generated LiveView and controller tests |

The **entry name** (`user`, `org`) is also the PubSub topic prefix (`"user:#{key}:posts"`) and the changeset's argument name (`user_scope`). The config must be in `config/config.exs` (or a file it imports). Generators never load `runtime.exs`.

`phx.gen.auth` takes `--scope name` and `--assign-key key` to rename its entry. It marks the entry `default: true` only if no default exists yet.

---

Running the Generators

| Command | Result | |---|---| | `mix phx.gen.live Blog Post posts title:string` | default scope | | `mix phx.gen.live Projects Project projects name:string --scope org` | the `org` entry | | `mix phx.gen.html Pages Page pages title:string --no-scope` | unscoped | | `--scope missing` | `** (Mix) Scope :missing not configured!` |

`phx.gen.live`, `phx.gen.html`, `phx.gen.json`, `phx.gen.context` and `phx.gen.schema` all take `--scope` / `--no-scope`.

A scoped run changes these files:

  • **Schema:** `field :user_id, :id` (not `belongs_to`) and `changeset(post, attrs, user_scope)`, which casts only your fields and ends with `put_change(:user_id, user_scope.user.id)`.
  • **Migration:** `add :user_id, references(:users, on_delete: :delete_all)` and `create index(:posts, [:user_id])`.
  • **Context:** every function takes `%Scope{}` first. `list_*` uses `Repo.all_by(Post, user_i
Read more
Ships withelixir-phoenix-guide

· Changelog A Claude Code plugin (with Codex support for the skills) that keeps Claude's Elixir and Phoenix code idiomatic and current.

Get the whole plugin

Other skills on elixir-phoenix-guide.