Per-project extension enablement — the hub's context layer.
The catalog (which extension types exist) lives in
PhoenixKitProjects.Extensions.Registry; this module owns the
per-project state: which extensions a given project has enabled, each
instance's config, and the enable/disable lifecycle (activity rows,
PubSub, provider callbacks).
Semantics (locked by the 2026-08-05 plan + panel round)
- Disable hides, never deletes —
disable/3flipsenabledto false; the row, itsconfig, and the extension's own data survive. Re-enabling restores everything. - Instance-keyed identity — every function threads
instance_key(default"default"); v1 UI exposes one instance per extension. - Config writes are whitelisted against the extension's declared
config_schemakeys — raw params never reach the JSONB. - Effective enablement is an intersection: a row with
enabled: truecounts only while the extension is in the catalog AND its backing site module is enabled (Registry.available?/1) — a site-level disable wins instantly without touching rows. - Defaults: a project with NO row for an extension falls back to the
catalog's
default_enabled(the built-in Tasks extension shipsdefault_enabled: true, preserving pre-hub behavior for every existing project). Presets (Step 3) write explicit rows at creation — absence always means "inherit the catalog default", never a frozen choice.
Authorization note: these functions trust their caller (the LiveView
layer gates on PhoenixKitProjects.Authz.can?/5 with :manage_modules).
Activity actor flows in via opts[:actor_uuid], LV-layer convention.
Summary
Functions
Disables an extension instance for a project. The row (and its config)
survives — see the moduledoc. Creates an explicit disabled row when the
enablement was implicit (catalog default_enabled), so "turn tasks off"
works on a project that never had a row.
Enables an extension for a project (upserts the instance row).
Whether one extension is effectively enabled for a project.
The extensions EFFECTIVELY enabled for a project: catalog availability ∩
(explicit row OR catalog default). Returns [{%Extension{}, %ProjectModule{} | nil}]
— the row is nil when enablement comes from default_enabled with no
explicit row yet.
One enablement row (or nil).
All enablement rows for a project (enabled AND disabled — panel UI needs both).
Updates one instance's config, whitelisted against the extension's
config_schema keys. Unknown keys are silently dropped (never written);
the stored map is REPLACED by the whitelisted input merged over it, so
omitted known keys survive.
Functions
@spec disable(map() | binary(), String.t(), keyword()) :: {:ok, PhoenixKitProjects.Schemas.ProjectModule.t()} | {:error, term()}
Disables an extension instance for a project. The row (and its config)
survives — see the moduledoc. Creates an explicit disabled row when the
enablement was implicit (catalog default_enabled), so "turn tasks off"
works on a project that never had a row.
@spec enable(map() | binary(), String.t(), keyword()) :: {:ok, PhoenixKitProjects.Schemas.ProjectModule.t()} | {:error, term()}
Enables an extension for a project (upserts the instance row).
Options: :actor_uuid, :instance_key, :config (whitelisted),
:name. Returns {:ok, row} or {:error, reason}. Unknown extension
keys are rejected ({:error, :unknown_extension}); an extension whose
site module is disabled is rejected ({:error, :module_disabled}) —
enabling something that can't render is a config trap.
Whether one extension is effectively enabled for a project.
Accepts a project struct or uuid. The intersection rule from the moduledoc applies; unknown keys are always false (fail-closed).
@spec enabled_for_project(binary()) :: [ {PhoenixKitProjects.Extensions.Extension.t(), PhoenixKitProjects.Schemas.ProjectModule.t() | nil} ]
The extensions EFFECTIVELY enabled for a project: catalog availability ∩
(explicit row OR catalog default). Returns [{%Extension{}, %ProjectModule{} | nil}]
— the row is nil when enablement comes from default_enabled with no
explicit row yet.
@spec get_row(binary(), String.t(), String.t()) :: PhoenixKitProjects.Schemas.ProjectModule.t() | nil
One enablement row (or nil).
@spec list_rows(binary()) :: [PhoenixKitProjects.Schemas.ProjectModule.t()]
All enablement rows for a project (enabled AND disabled — panel UI needs both).
@spec update_config(map() | binary(), String.t(), map(), keyword()) :: {:ok, PhoenixKitProjects.Schemas.ProjectModule.t()} | {:error, term()}
Updates one instance's config, whitelisted against the extension's
config_schema keys. Unknown keys are silently dropped (never written);
the stored map is REPLACED by the whitelisted input merged over it, so
omitted known keys survive.