Scope module for encapsulating PhoenixKit authentication state.
This module provides a structured way to handle user authentication context throughout your Phoenix application, similar to Phoenix's built-in authentication patterns but with PhoenixKit prefixing to avoid conflicts.
Nil scopes
Every predicate and accessor here tolerates a nil scope and answers as if
the visitor were unauthenticated. Host layouts render on kit auth pages with
phoenix_kit_current_scope unset, so authenticated?(nil) and
anonymous?(nil) are ordinary calls, not programmer errors — and "no scope"
and "a scope with no user" mean the same thing to a caller deciding what to
render.
The exception is to_map/1, which deliberately does not accept nil. There
is nothing meaningful to serialize for a non-scope, and returning an empty map
would fabricate data that looks real; it is a debug/serialization helper, so
failing surfaces the bug where permissiveness would bury it. Its spec keeps
t(), so the type checker rejects to_map(nil) at compile time rather than
leaving it to blow up at runtime.
Apply the same rule to anything added here: if nil has a correct answer,
give it one; if answering would mean inventing data, let it raise.
Usage
# Create scope for authenticated user
scope = Scope.for_user(user)
# Create scope for anonymous user
scope = Scope.for_user(nil)
# Check authentication status
Scope.authenticated?(scope) # true or false
# Get user information
Scope.user(scope) # %User{} or nil
Scope.user_uuid(scope) # user.uuid or nil
Scope.user_email(scope) # user.email or nilRole & State Checks
Scope.has_role?(scope, "Admin") # true/false
Scope.owner?(scope) # Owner role?
Scope.can_access_admin_area?(scope) # Owner, Admin, or custom role with permissions?
Scope.system_role?(scope) # Strictly Owner or Admin (not custom roles)?
Scope.anonymous?(scope) # Not authenticated?
Scope.user_roles(scope) # ["Admin", "User"]
Scope.user_full_name(scope) # "John Doe" or nil
Scope.user_active?(scope) # true/false
Scope.to_map(scope) # Debug-friendly map of all fieldsModule-Level Permissions
Permissions are cached in the scope when it is built via for_user/1
(on mount and on PubSub-triggered refresh). Owner gets every key
automatically. Admin defaults to all keys via seeding/auto-grant but is
genuinely gated by its rows — the full-access fallback applies only on an
unseeded install (no permission rows exist at all).
Scope.has_module_access?(scope, "billing") # Single key check (pure cache)
Scope.can?(scope, "calendar.view_others") # Key held AND module enabled
Scope.has_any_module_access?(scope, ["billing", "shop"]) # Any of these?
Scope.has_all_module_access?(scope, ["billing", "shop"]) # All of these?
Scope.accessible_modules(scope) # MapSet of granted keys
Scope.permission_count(scope) # Number of granted keysStruct Fields
:user- The current user struct or nil:authenticated?- Boolean indicating if user is authenticated:cached_roles- List of role name strings IN EFFECT, loaded at scope creation:cached_permissions- MapSet of granted permission keys, loaded at scope creation:held_roles- List of every role name the account really holds:active_role-%{uuid, name}of the role the user is acting as, ornil:switchable_roles-%{uuid, name}of every role the user can switch to ([]when not narrowed):role_records-%{uuid, name, data}of the roles IN EFFECT, for showing them translated (Roles.display_name/2) without a query:acting_link- The person's link while acting for an organization, ornil(PhoenixKit.Users.ActingLink)
Active role
With the role switcher on, a user holding two or more switchable roles acts
as one of them, and for_user/1 narrows cached_roles and
cached_permissions to that role plus the always-on ones — so every check
above answers for the active role. See PhoenixKit.Users.ActiveRole.
Scope.active_role(scope) # %{uuid: "...", name: "Seller"} or nil
Scope.held_roles(scope) # ["Admin", "Seller", "User"]
Scope.narrowed?(scope) # acting as one role?held_roles/1 is for rules that protect against an account's REAL roles (e.g.
"cannot edit a role you hold"). Never ask it what someone may do: access decisions
use the narrowed checks. The switcher reads switchable_roles/1.
for_user(user, narrow: false) builds the full union.
Acting for an organization
A person's session in a managed organization is the organization's, narrowed twice:
first by the managed ceiling, then by their link. cached_roles, cached_permissions
and the switcher all follow both, so a person with only Supplier on their link has
no switcher at a company holding Buyer and Supplier.
⚠️ held_roles/1 follows neither: there it is the company's roles, Buyer included.
To ask what the person has, read user_roles/1 or has_role?/2.
Scope.acting_link(scope) # %{type_key: "staff", role_uuids: [...], ...} or nil
Scope.can_manage_organization?(scope) # may they manage its people and profile?A managed organization's scope built from anything but a session holds only the roles
every link allows (ActingLink.outside_session/1), so it is never wider than a session.
Summary
Functions
Returns the set of module keys the user can access.
The link of an acting session (PhoenixKit.Users.ActingLink): its type_key,
can_manage?, all_roles? and role_uuids. nil outside an acting session.
The person behind an acting organization session; nil for an ordinary account.
The role the user is acting as (%{uuid, name}), or nil when the scope is
not narrowed. See PhoenixKit.Users.ActiveRole.
Deprecated alias for can_access_admin_area?/1.
Checks if the scope represents an anonymous (non-authenticated) user.
Checks if the scope represents an authenticated user.
Checks whether the user holds a permission key AND that key is currently
effective — the module behind it (or behind its parent, for sub-permission
keys like "calendar.view_others") is enabled.
Checks if the user can access the admin AREA — the /admin shell entry gate.
Whether the person may manage the organization they act for: its people and its profile. For what to show: a change re-reads the link.
Creates a new scope for the given user.
Checks if the user has access to all of the given module keys.
Checks if the user has access to at least one of the given module keys.
Checks if the user has access to a specific admin module/section.
Checks if the user has a specific role.
Every role the account really holds, whatever role it is acting as.
Whether this scope holds EVERY currently-grantable permission key — i.e. it
can reach everything the permission system exposes right now, exactly like
Owner. Purely permission-based and role-AGNOSTIC: a custom role granted all
permissions returns true, with no role-name special-casing.
Whether the scope is narrowed to an active role.
Checks if the user is an owner.
Returns the number of module permissions the user has been granted.
The account label for the current browser, including organization provenance.
Whether the scope holds the wildcard superadmin key ("*") — a blanket,
drift-immune grant to every permission-gated FEATURE/VIEW. Owner holds it by
construction; a host can grant it to a custom role to make that role
Owner-equivalent for feature access with one grant, and unlike a "grant every
current key" role it stays complete as new modules are added.
The roles the user can switch between (%{uuid, name}), loaded with the
scope so the switcher renders without a query. [] when not narrowed.
Checks if the user holds the Owner or Admin system role.
Converts scope to a map for debugging or logging purposes.
Gets the user from the scope.
Checks if the user is active.
Gets the user email from the scope.
Gets the user's full name.
Gets all roles for the user.
Gets the user ID (UUID) from the scope.
Types
@type t() :: %PhoenixKit.Users.Auth.Scope{ acting_link: PhoenixKit.Users.ActingLink.t() | nil, acting_organizations: term(), acting_person: term(), active_role: PhoenixKit.Users.ActiveRole.role() | nil, authenticated?: boolean(), cached_permissions: MapSet.t() | nil, cached_roles: [String.t()] | nil, held_roles: [String.t()] | nil, impersonated_by_uuid: String.t() | nil, multi_session_accounts: list(), multi_session_allowed?: boolean(), organization_root: term(), pending_invitations: term(), role_records: [PhoenixKit.Users.ActiveRole.role()], switchable_roles: [PhoenixKit.Users.ActiveRole.role()], user: PhoenixKit.Users.Auth.User.t() | nil }
Functions
Returns the set of module keys the user can access.
@spec acting_link(t() | nil) :: PhoenixKit.Users.ActingLink.t() | nil
The link of an acting session (PhoenixKit.Users.ActingLink): its type_key,
can_manage?, all_roles? and role_uuids. nil outside an acting session.
The person behind an acting organization session; nil for an ordinary account.
@spec active_role(t() | nil) :: PhoenixKit.Users.ActiveRole.role() | nil
The role the user is acting as (%{uuid, name}), or nil when the scope is
not narrowed. See PhoenixKit.Users.ActiveRole.
Deprecated alias for can_access_admin_area?/1.
The name misleads: it returns true for ANY permission holder, not just the
Admin role. Call can_access_admin_area?/1 instead.
Checks if the scope represents an anonymous (non-authenticated) user.
Examples
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.anonymous?(scope)
true
iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.anonymous?(scope)
false
Checks if the scope represents an authenticated user.
Examples
iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.authenticated?(scope)
true
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.authenticated?(scope)
false
Checks whether the user holds a permission key AND that key is currently
effective — the module behind it (or behind its parent, for sub-permission
keys like "calendar.view_others") is enabled.
This is the check modules should use for fine-grained, in-page
authorization. Unlike has_module_access?/2 (a pure cache lookup used on
hot paths where enablement is enforced separately at mount), can?/2
consults live module-enablement state, so a scope snapshotted before a
module was disabled cannot keep authorizing its actions.
Examples
Scope.can?(scope, "calendar.edit_others")
Scope.can?(scope, "calendar")
Checks if the user can access the admin AREA — the /admin shell entry gate.
Returns true when the user holds the Admin or Owner role, OR has been
explicitly granted any module-level permission (via RolePermission) — so a
custom role (e.g. "Editor", "Support") holding at least one permission can
enter the admin area.
This is a COARSE entry gate only. It does NOT mean the user is a privileged
operator: holding a single grant is enough. Which pages and actions are
actually allowed is enforced per-view by has_module_access?/2 / can?/2.
For a "can do everything, like Owner" check use holds_all_enabled_permissions?/1
or superadmin?/1 instead — never this.
Examples
iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.can_access_admin_area?(scope)
true
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.can_access_admin_area?(scope)
false
Whether the person may manage the organization they act for: its people and its profile. For what to show: a change re-reads the link.
@spec for_user(PhoenixKit.Users.Auth.User.t() | nil, keyword()) :: t()
Creates a new scope for the given user.
Examples
iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001", email: "user@example.com"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> scope.authenticated?
true
iex> scope.user.email
"user@example.com"
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> scope.authenticated?
false
iex> scope.user
nil
Checks if the user has access to all of the given module keys.
Examples
Scope.has_all_module_access?(scope, ["billing", "shop"])
Checks if the user has access to at least one of the given module keys.
Examples
Scope.has_any_module_access?(scope, ["billing", "shop"])
Checks if the user has access to a specific admin module/section.
Looks up module_key in cached_permissions. Owner access works because
for_user/1 pre-populates all keys for owners; this function itself does
not special-case roles.
Checks if the user has a specific role.
Examples
iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.has_role?(scope, "Admin")
true
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.has_role?(scope, "Admin")
false
Every role the account really holds, whatever role it is acting as.
For rules that must judge the account's REAL roles (e.g. "you cannot edit a role
you hold"). Access decisions belong to the narrowed checks (user_roles/1,
has_role?/2, can_access_admin_area?/1, ...), never this.
⚠️ In an acting session the account is the organization: this is every role the organization holds, including roles outside the person's link and above the managed ceiling. It says nothing about what the person may do.
A scope built without held_roles answers with its cached_roles.
Whether this scope holds EVERY currently-grantable permission key — i.e. it
can reach everything the permission system exposes right now, exactly like
Owner. Purely permission-based and role-AGNOSTIC: a custom role granted all
permissions returns true, with no role-name special-casing.
Compared against Permissions.enabled_module_keys/0 (the grantable set the
permissions matrix produces), NOT all_module_keys/0. Disabled modules
expose no reachable admin surface and their keys can't be granted via the UI,
so a grant-all custom role legitimately lacks them — including them would
re-introduce the Owner-vs-custom asymmetry this is meant to remove (Owner's
set is a superset that trivially satisfies the subset test either way). The
size > 0 guard stops an empty grantable set from making subset?/2
vacuously true (a fail-open) for every scope; in practice the core and
integration keys are a non-disableable floor, but the guard never rots.
Whether the scope is narrowed to an active role.
Checks if the user is an owner.
Examples
iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.owner?(scope)
true
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.owner?(scope)
false
@spec permission_count(t()) :: non_neg_integer()
Returns the number of module permissions the user has been granted.
The account label for the current browser, including organization provenance.
Whether the scope holds the wildcard superadmin key ("*") — a blanket,
drift-immune grant to every permission-gated FEATURE/VIEW. Owner holds it by
construction; a host can grant it to a custom role to make that role
Owner-equivalent for feature access with one grant, and unlike a "grant every
current key" role it stays complete as new modules are added.
Scope: feature/view access only. The Owner-only role-management safety rails
(editing the Admin role, assigning Owner/Admin, the last-Owner guard) remain
role-name-based by design and are NOT unlocked by "*".
@spec switchable_roles(t() | nil) :: [PhoenixKit.Users.ActiveRole.role()]
The roles the user can switch between (%{uuid, name}), loaded with the
scope so the switcher renders without a query. [] when not narrowed.
Checks if the user holds the Owner or Admin system role.
Unlike admin?/1 which also returns true for custom roles with permissions,
this strictly checks for the two built-in system roles.
Converts scope to a map for debugging or logging purposes.
Examples
iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001", email: "user@example.com"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.to_map(scope)
%{
authenticated?: true,
user_uuid: "019...",
user_email: "user@example.com",
user_roles: ["Admin", "User"],
owner?: false,
admin?: true
}
@spec user(t() | nil) :: PhoenixKit.Users.Auth.User.t() | nil
Gets the user from the scope.
Examples
iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001", email: "user@example.com"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.user(scope)
%PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001", email: "user@example.com"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.user(scope)
nil
Checks if the user is active.
Examples
iex> user = %PhoenixKit.Users.Auth.User{is_active: true}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.user_active?(scope)
true
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.user_active?(scope)
false
Gets the user email from the scope.
Examples
iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001", email: "user@example.com"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.user_email(scope)
"user@example.com"
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.user_email(scope)
nil
Gets the user's full name.
Examples
iex> user = %PhoenixKit.Users.Auth.User{first_name: "John", last_name: "Doe"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.user_full_name(scope)
"John Doe"
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.user_full_name(scope)
nil
Gets all roles for the user.
Examples
iex> user = %PhoenixKit.Users.Auth.User{uuid: "0193a5e4-0000-7000-8000-000000000001"}
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.user_roles(scope)
["Admin", "User"]
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.user_roles(scope)
[]
Gets the user ID (UUID) from the scope.
Examples
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(user)
iex> PhoenixKit.Users.Auth.Scope.user_uuid(scope)
"0193a5e4-..."
iex> scope = PhoenixKit.Users.Auth.Scope.for_user(nil)
iex> PhoenixKit.Users.Auth.Scope.user_uuid(scope)
nil