Runtime registry of all PhoenixKit modules (internal and external).
External modules are auto-discovered from beam files via PhoenixKit.ModuleDiscovery.
Any dep that depends on :phoenix_kit and uses use PhoenixKit.Module is found
automatically — no config line needed.
External Module Registration
External hex packages are auto-discovered. Just add the dep, at the package's current release:
{:phoenix_kit_hello_world, "~> X.Y"}For explicit registration (backwards compatible):
config :phoenix_kit, :modules, [PhoenixKitHelloWorld]Runtime Registration
PhoenixKit.ModuleRegistry.register(MyModule)
PhoenixKit.ModuleRegistry.unregister(MyModule)Query API
ModuleRegistry.all_modules() # All registered module atoms
ModuleRegistry.enabled_modules() # Only currently enabled
ModuleRegistry.all_admin_tabs() # Collect admin tabs from all modules
ModuleRegistry.all_settings_tabs() # Collect settings tabs
ModuleRegistry.all_email_settings_sections() # Collect Emails Transactional page sections
ModuleRegistry.all_email_templates() # Collect emails for the admin email preview
ModuleRegistry.all_api_tools() # Collect API tools of enabled modules
ModuleRegistry.all_api_problems() # Collect API error codes of enabled modules
ModuleRegistry.all_header_widgets() # Collect admin header widgets of enabled modules
ModuleRegistry.all_user_dashboard_tabs() # Collect user dashboard tabs
ModuleRegistry.all_children() # Collect supervisor child specs
ModuleRegistry.all_permission_metadata() # Collect permission metadata
ModuleRegistry.feature_enabled_checks() # Build {mod, :enabled?} map
ModuleRegistry.get_by_key("tickets") # Find module by key
ModuleRegistry.get_module_key_for_namespace("PhoenixKitTickets")
# Resolve top-level namespace → key
Summary
Functions
Collect all admin tabs from all registered modules.
Collect the API error codes (PhoenixKit.Module.api_problems/0) of the enabled
modules as %{code => %{status, title, advice}}.
Collect the API tools (PhoenixKit.Module.api_tools/0) of the enabled modules,
each tagged with :module, the module that declared it.
Collect all supervisor child specs from all registered modules.
Collect email settings sections contributed by all enabled modules,
for the core Emails Transactional settings page
(/admin/settings/email-sending).
Collect the emails enabled modules send (PhoenixKit.Module.email_templates/0),
each tagged with :module, the module that declared it.
Returns all feature module key strings from registered modules.
Collect the admin header widgets (PhoenixKit.Module.header_widgets/0) of the
enabled modules, as PhoenixKit.HeaderWidget structs tagged with :module.
The kinds of job run (PhoenixKit.Jobs.Kind) the enabled modules declare
with PhoenixKit.Module.job_kinds/0. A kind that is not a loaded module is
left out, with one warning.
Collect media reorganizer source modules contributed by all enabled modules.
Returns all registered module atoms.
Collect permission metadata from all registered modules.
Collect the profile settings tabs (PhoenixKit.Module.profile_settings_tabs/0) of the
enabled modules, as PhoenixKit.ProfileSettingsTab structs tagged with :module.
Duplicate ids are resolved before enablement, matching the compiled routes: disabling
the first declaring module cannot hand its URL to a different module's tab.
Only modules whose declarations survive are asked whether they are enabled.
Collect top-level route path segments reserved by all installed modules.
Collect route modules from all registered modules.
Collect all settings tabs from all registered modules.
Collect sitemap source modules contributed by all enabled modules.
Collect all user dashboard tabs from all registered modules.
The key a module's API switches are stored under (its module_key/0), or nil for a
module without a usable one. PhoenixKit.API.Executor reads it to match a tool's
:module against api_session_write_modules.
Every registered module that declares at least one valid API tool, switched on or off, in
registration order: %{module, key, name, enabled?, tools}, where tools are its tools
tagged with :module (validated as in all_api_tools/1) and key is nil for a module
without a usable module_key/0.
The registered modules that declare at least one valid API tool, switched on or off,
sorted by name: %{module, key, name, enabled?, tools, audiences, writes?} (tools is
how many, audiences the audiences they serve, writes? whether one of them is an admin
tool that writes, PhoenixKit.API.ControlFields.write_tool?/1).
The names of the API tools declared for audience by every registered module,
switched on or off, sorted and without duplicates.
Returns a specification to start this module under a supervisor.
Returns dependency warnings for the Modules page.
True when a registered module that is switched off declares the API tool name for
audience.
Returns all registered modules that are currently enabled.
Build a feature_enabled_checks map from registered modules.
Find a registered module by its key string.
Find a registered module's key by matching a top-level Elixir namespace.
The header widgets to render for one layout render: all_header_widgets/0, each with
:session computed by its session function from assigns (the layout's assigns).
Check if the registry has been initialized.
Returns the full catalog of known external PhoenixKit packages.
Collect modules that have versioned migrations.
Returns known external PhoenixKit packages that are not currently installed.
Returns permission descriptions map from registered modules.
Returns the optional gettext {backend, domain} pairs registered modules
declare for translating their permission labels (see
PhoenixKit.Module.permission_meta/0). Modules without a
gettext_backend are absent from the map.
Returns permission icons map from registered modules.
Returns permission labels map from registered modules.
Register a module that implements PhoenixKit.Module behaviour.
Rescan beam files and absorb any external modules that weren't known at registry init.
Run every enabled module's migrate_legacy/0 callback.
Collect supervisor child specs from the static module list.
Returns the sub-permissions declared by registered modules, keyed by the
parent module key. Each entry carries the COMPOSED dotted key
("calendar.view_others") plus its label and description.
Unregister a module.
Whether a module base permission key is well-formed (lowercase, no dots, within length). A dotted base key would collide with composed sub-keys, so the permission plumbing ignores base keys that fail this check.
Functions
@spec all_admin_tabs() :: [PhoenixKit.Dashboard.Tab.t()]
Collect all admin tabs from all registered modules.
Note: iterates all modules and calls admin_tabs/0 on each call.
For cached access in rendering paths, use PhoenixKit.Dashboard.Registry.get_admin_tabs/0.
@spec all_api_problems() :: %{ required(atom()) => %{ status: 400..599, title: String.t(), advice: String.t() | nil } }
Collect the API error codes (PhoenixKit.Module.api_problems/0) of the enabled
modules as %{code => %{status, title, advice}}.
This is read when an error is answered (PhoenixKit.API.Problem), never on the success
path. A code that is one of core's (PhoenixKit.API.Problem.core_code?/1) cannot be
overridden, so that declaration is dropped with a warning; so is an entry with a code that
is not an atom, a status outside 400..599, a title that is not a non-empty string
or an advice that is neither a string nor nil. A code declared by two modules keeps
the first (registration order), again with one warning. A module whose
api_problems/0 raises, or returns something that is not a map, contributes nothing
and is reported once.
Like all_api_tools/1 it asks every module again on every call.
@spec all_api_tools([String.t()]) :: [PhoenixKit.API.Tool.t()]
Collect the API tools (PhoenixKit.Module.api_tools/0) of the enabled modules,
each tagged with :module, the module that declared it.
A disabled module's tools cannot be called, so they are left out (disabled_api_tool?/3
is the other side of that). So are the tools of a module whose key is in hidden_keys
(the install's api_disabled_modules setting: the module's API is switched off). An entry
that is not a valid PhoenixKit.API.Tool (PhoenixKit.API.Tool.validate/1) is dropped
with one warning per module, and so is a tool in core's system. namespace (those skip
the scope and permission checks and belong to PhoenixKit.API.CoreTools alone). A name
declared twice for one audience keeps the first (registration order), again with one
warning; the keys are removed before that, so switching the first module off hands the
name to the second. A module whose api_tools/0 raises contributes nothing and is
reported once.
This is not a gate. With the default hidden_keys it returns the tools of modules whose
API is switched off; only PhoenixKit.API.Registry applies the install's settings, so a
transport must ask it for tools.
Every call asks every module again (enabled?/0 and api_tools/0 are not cached), so a
caller that serves requests should call it once per request and keep the result.
@spec all_children() :: [Supervisor.child_spec()]
Collect all supervisor child specs from all registered modules.
@spec all_email_settings_sections() :: [PhoenixKit.Module.email_settings_section()]
Collect email settings sections contributed by all enabled modules,
for the core Emails Transactional settings page
(/admin/settings/email-sending).
Mirrors all_settings_tabs/0's safe_call pattern, but — like
all_sitemap_sources/0 — iterates enabled_modules/0 rather than
all_modules/0: a disabled module must not inject a live_component
that assumes its own supervised state is running.
@spec all_email_templates() :: [map()]
Collect the emails enabled modules send (PhoenixKit.Module.email_templates/0),
each tagged with :module, the module that declared it.
Enabled modules only, like all_email_settings_sections/0: a disabled
module sends nothing, so the preview has none of its emails to show.
@spec all_feature_keys() :: [String.t()]
Returns all feature module key strings from registered modules.
@spec all_header_widgets() :: [PhoenixKit.HeaderWidget.t()]
Collect the admin header widgets (PhoenixKit.Module.header_widgets/0) of the
enabled modules, as PhoenixKit.HeaderWidget structs tagged with :module.
Only a module that defines header_widgets/0 is asked (and only then is its enabled?/0
called), so an install without such modules pays nothing for this on the layout's render
path. An entry that is not a valid widget is dropped with one warning per module, a widget
whose id was already taken by an earlier one is dropped with one warning, and a module
whose header_widgets/0 raises contributes nothing and is reported once.
@spec all_job_kinds() :: [module()]
The kinds of job run (PhoenixKit.Jobs.Kind) the enabled modules declare
with PhoenixKit.Module.job_kinds/0. A kind that is not a loaded module is
left out, with one warning.
@spec all_media_reorganizers() :: [module()]
Collect media reorganizer source modules contributed by all enabled modules.
Each entry implements PhoenixKit.Modules.Storage.Reorganizer.Source.
Iterates enabled_modules/0, like all_sitemap_sources/0: a disabled
module's records must not be touched by a reorganizer run.
@spec all_modules() :: [module()]
Returns all registered module atoms.
@spec all_permission_metadata() :: [PhoenixKit.Module.permission_meta()]
Collect permission metadata from all registered modules.
@spec all_profile_settings_tabs() :: [PhoenixKit.ProfileSettingsTab.t()]
Collect the profile settings tabs (PhoenixKit.Module.profile_settings_tabs/0) of the
enabled modules, as PhoenixKit.ProfileSettingsTab structs tagged with :module.
Duplicate ids are resolved before enablement, matching the compiled routes: disabling
the first declaring module cannot hand its URL to a different module's tab.
Only modules whose declarations survive are asked whether they are enabled.
@spec all_reserved_route_prefixes() :: [String.t()]
Collect top-level route path segments reserved by all installed modules.
Each entry is a literal path segment (no slashes, e.g. "legal") a module
owns for its own LiveViews/controllers. A dispatcher that routes requests
based on database-driven path segments (e.g. Publishing's group catch-all)
should treat any segment in this list as NOT its own, even if it happens to
have matching data, so it doesn't swallow a route another module owns.
Declaring a prefix is passive — it changes nothing unless a dispatcher
actively consults this function.
Iterates all_modules/0, not enabled_modules/0 (unlike
all_sitemap_sources/0): the route this guards against is normally
compiled into the host's router (e.g. live "/legal", LegalLive), which
exists independent of the owning module's runtime enabled/disabled
Settings toggle. Gating on enabled_modules/0 would drop the reservation
the moment a module is disabled while its data (and the host's compiled
route) still exist, reopening the exact hijack this is meant to prevent
for as long as the module stays disabled.
Each module's returned segments are trimmed of leading/trailing slashes
and non-list/non-string returns are dropped, so a malformed
reserved_route_prefixes/0 implementation (e.g. returning "/legal" or a
bare string instead of a list) can't silently defeat the reservation or
crash this per-request-hot-path aggregation.
@spec all_route_modules() :: [module()]
Collect route modules from all registered modules.
@spec all_settings_tabs() :: [PhoenixKit.Dashboard.Tab.t()]
Collect all settings tabs from all registered modules.
@spec all_sitemap_sources() :: [module()]
Collect sitemap source modules contributed by all enabled modules.
Each entry implements PhoenixKit.Modules.Sitemap.Sources.Source. The
sitemap Generator appends these to its base source list (deduplicated)
so module-owned content appears in the sitemap with no host config.
Iterates enabled_modules/0, so a disabled module contributes nothing — even
in flat-sitemap mode, where the Generator deliberately force-collects every
source and bypasses each source's own enabled?/0 (force: true, see
Sources.Source.safe_collect/2 and Generator flat-mode generation). Gating
at the module level here keeps the "disabled module emits no URLs" guarantee
in both modes. A contributed source's own enabled?/0 remains a secondary
gate in index mode (see Generator.generate_module/2).
@spec all_user_dashboard_tabs() :: [PhoenixKit.Dashboard.Tab.t()]
Collect all user dashboard tabs from all registered modules.
The key a module's API switches are stored under (its module_key/0), or nil for a
module without a usable one. PhoenixKit.API.Executor reads it to match a tool's
:module against api_session_write_modules.
@spec api_module_tools() :: [ %{ module: module(), key: String.t() | nil, name: String.t(), enabled?: boolean(), tools: [PhoenixKit.API.Tool.t(), ...] } ]
Every registered module that declares at least one valid API tool, switched on or off, in
registration order: %{module, key, name, enabled?, tools}, where tools are its tools
tagged with :module (validated as in all_api_tools/1) and key is nil for a module
without a usable module_key/0.
For enumeration only (what could be offered, and what keeps it closed); it is not a gate:
PhoenixKit.API.Registry decides what is callable.
@spec api_modules() :: [ %{ module: module(), key: String.t(), name: String.t(), enabled?: boolean(), tools: pos_integer(), audiences: [PhoenixKit.API.Tool.audience()], writes?: boolean() } ]
The registered modules that declare at least one valid API tool, switched on or off,
sorted by name: %{module, key, name, enabled?, tools, audiences, writes?} (tools is
how many, audiences the audiences they serve, writes? whether one of them is an admin
tool that writes, PhoenixKit.API.ControlFields.write_tool?/1).
For the screens that list them (the API switch per module in Settings -> API, the badge
on the Modules page). It is not a gate: PhoenixKit.API.Registry decides what is callable.
@spec api_tool_names(:admin | :public) :: [String.t()]
The names of the API tools declared for audience by every registered module,
switched on or off, sorted and without duplicates.
This is for enumeration only (for instance a screen listing the tools that can be hidden
or granted). It is not a gate: PhoenixKit.API.Registry.find/2 decides whether a tool
can be called. The tools go through the same validation as in all_api_tools/0, so an
invalid tool and a system. name declared by a module are left out, and a module whose
api_tools/0 raises contributes nothing.
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec dependency_warnings() :: [map()]
Returns dependency warnings for the Modules page.
Each warning is a map: %{module: module(), module_name: String.t(), requires_key: String.t()}
Called on Modules page render — computes live (not hot path).
True when a registered module that is switched off declares the API tool name for
audience.
This is the "module disabled" test of PhoenixKit.API.Registry.find/2. It asks only the
disabled modules for their tools, and it looks at the audience, so an anonymous caller
cannot learn that an admin tool exists by asking for it on the public side. A module
whose key is in hidden_keys (its API is switched off) does not count: its tools answer
like a name nobody has.
@spec enabled_modules() :: [module()]
Returns all registered modules that are currently enabled.
Build a feature_enabled_checks map from registered modules.
Returns %{"storage" => {PhoenixKit.Modules.Storage, :enabled?}, ...}
Find a registered module by its key string.
Find a registered module's key by matching a top-level Elixir namespace.
Used by the admin permission layer to resolve a plugin LiveView's namespace
(e.g. "PhoenixKitEntities" from PhoenixKitEntities.Web.Entities) to the
plugin's permission key (e.g. "entities").
Returns the key string or nil when no registered module matches.
@spec header_widgets_for(map()) :: [PhoenixKit.HeaderWidget.t()]
The header widgets to render for one layout render: all_header_widgets/0, each with
:session computed by its session function from assigns (the layout's assigns).
A widget whose session function returns nil is dropped without a word (no widget for
this user); one whose session function raises, or returns something that is not a map with
string keys, is dropped with one warning. The keys the cookie session owns
(PhoenixKit.HeaderWidget.reserved_session_keys/0) are removed from the session, with one
warning per module and widget.
@spec initialized?() :: boolean()
Check if the registry has been initialized.
@spec known_external_packages() :: [map()]
Returns the full catalog of known external PhoenixKit packages.
Fetches live from Hex.pm with a 10-minute in-memory cache.
Merged with any entries from config :phoenix_kit, extra_known_packages: [...].
Config entries take precedence over Hex entries when the package field collides.
Collect modules that have versioned migrations.
Returns a list of {module_name, migration_module} tuples for all registered
modules that implement migration_module/0 and return a non-nil value.
@spec not_installed_packages() :: [map()]
Returns known external PhoenixKit packages that are not currently installed.
Used by the admin Modules page to inform users about available packages they can add as dependencies.
Returns permission descriptions map from registered modules.
Returns the optional gettext {backend, domain} pairs registered modules
declare for translating their permission labels (see
PhoenixKit.Module.permission_meta/0). Modules without a
gettext_backend are absent from the map.
Returns permission icons map from registered modules.
Returns permission labels map from registered modules.
@spec register(module()) :: :ok
Register a module that implements PhoenixKit.Module behaviour.
Rescan beam files and absorb any external modules that weren't known at registry init.
Returns {:ok, new_modules} — the freshly-absorbed module atoms (often
[] after the first call). Safe to call repeatedly.
Intended use: parent app calls this from Application.start/2 after
Supervisor.start_link/2 so late-loading :phoenix_kit_<x> deps
whose beams are only available after PhoenixKit's own supervision tree
is up get picked up deterministically — without timer-based polling.
mix phoenix_kit.install and mix phoenix_kit.update wire this call
in automatically; existing apps can call it manually.
Also useful in dev for hot-reload recovery after recompiling a module package, or in tests that dynamically load fixture modules.
Cheap when nothing changed: the disk is checked with stat and the beams are
read only when a dep was added, removed or rebuilt since the last scan
(PhoenixKit.ModuleDiscovery.current_external_modules/0). timeout bounds the
call for the case where it does have to scan (default one minute).
Run every enabled module's migrate_legacy/0 callback.
Iterates registered modules, calls migrate_legacy/0 on each that
implements it, swallows per-module errors so the host-app boot can't
be taken down by a flaky migration. Each module's implementation is
expected to be idempotent (safe to re-run on every boot).
Activity logging happens inside each module's migrate_legacy/0 —
this orchestrator only logs the per-module pass/fail outcome to the
Logger, not to Activity.
PhoenixKit.boot/1 calls it — pipe the supervisor result into that from
the host's Application.start/2 (mix phoenix_kit.install / update wire
it in). A host that still calls this function itself after
Supervisor.start_link/2 gets the same behaviour.
Does nothing with update_mode on (mix phoenix_kit.update and
mix phoenix_kit.doctor start the host that way) and returns %{}. The
callbacks are one-shot data transitions whose "already done" checks
usually read PhoenixKit.Settings, which answers nil in that mode while
writes still go through — so they redid their work on every update or
doctor run (granting back a permission an Owner had revoked, re-stamping
markers). The update task also starts the host before it migrates, so
they would run against the old schema. The host's next ordinary start
runs them. The check is here, not at a call site, so boot/1, a direct
call and the deprecated PhoenixKit.Integrations.run_legacy_migrations/0
are all covered.
Returns a summary map: %{module_atom => :ok | {:error, term()}}.
@spec static_children() :: [Supervisor.child_spec()]
Collect supervisor child specs from the static module list.
This does NOT require the GenServer to be running, making it safe to call from the PhoenixKit.Supervisor init (before the registry starts).
Returns the sub-permissions declared by registered modules, keyed by the
parent module key. Each entry carries the COMPOSED dotted key
("calendar.view_others") plus its label and description.
Malformed declarations (bad key format, missing fields, key over 50 chars, duplicate keys within one module) are dropped with a logged warning rather than raising — a broken module must not take down permission resolution for everyone else.
@spec unregister(module()) :: :ok
Unregister a module.
Whether a module base permission key is well-formed (lowercase, no dots, within length). A dotted base key would collide with composed sub-keys, so the permission plumbing ignores base keys that fail this check.