# `PhoenixKitEntities.Managed`
[🔗](https://github.com/BeamLabEU/phoenix_kit_entities/blob/v0.4.6/lib/phoenix_kit_entities/managed.ex#L1)

Support for MANAGED entity blueprints — blueprints owned by another
module (e.g. the catalogue's attribute sets), created and administered
exclusively through that module's own UI on top of the entities API.

A managed blueprint carries in its `settings`:

    "managed_by"  => "catalogue"          # owning module key
    "locked_keys" => ["kind", ...]        # settings["<owner>"] keys the
                                          # generic write path must not touch

## The two guarantees

1. **Hidden from the generic admin** — `managed?/1` lets listings
   exclude these blueprints (`PhoenixKitEntities.list_entities/1`
   accepts `include_managed: false`); the owning module renders its
   own management UI.
2. **Write-path protection** — `validate_mutation/2` and
   `validate_delete/1` are called by the entities write path (not just
   the UI): mutations from anywhere but the owning module cannot
   rename the blueprint's slug, change its status, or touch locked
   settings keys; deletion requires the owner's confirmation callback
   to approve (e.g. the catalogue refuses while item attachments
   exist). UI guards without a write interceptor are theater.
   One write path is deliberately outside the interception:
   `reorder_entities/2` bulk-updates `position` via `update_all`, and
   `position` is not part of any owner contract.

Owners bypass the guard by passing `on_behalf_of: "<owner>"` in opts —
the guard is against *accidental* generic-admin edits, not a security
boundary (all callers are admin code). It also covers only the
blueprint rows themselves — data records under a managed blueprint go
through the ordinary `EntityData` write path.

## Delete approval

Owners register a delete-approval callback at runtime:

    PhoenixKitEntities.Managed.register_delete_guard(
      "catalogue",
      &MyApp.Catalogue.deletion_guard/1
    )

where `deletion_guard/1` returns `:ok` or `{:error, reason}` (e.g.
`{:error, :set_in_use}` while item attachments exist). The callback
MUST be an external capture (`&Mod.fun/1`), never an anonymous fun: a
local capture in `:persistent_term` goes stale when the registering
module is purged (code reload / hot upgrade), raises on call, and
every delete then fails closed with `{:error, :delete_guard_error}`.

Registration is process-independent (persistent_term), set up in the
owning module's application start. A managed blueprint with no
registered guard refuses deletion outright — fail closed.

# `managed?`

```elixir
@spec managed?(struct() | map()) :: boolean()
```

True when the entity is managed by another module.

# `owner`

```elixir
@spec owner(struct() | map()) :: String.t() | nil
```

The owning module key, or nil.

# `register_delete_guard`

```elixir
@spec register_delete_guard(String.t(), (struct() -&gt; :ok | {:error, term()})) :: :ok
```

Registers (replaces) the owner's delete-approval callback.

# `validate_creation`

```elixir
@spec validate_creation(
  map(),
  keyword()
) :: :ok | {:error, :managed_blueprint}
```

Validates CREATING an entity with `attrs`: a blueprint claiming a
`managed_by` owner can only be provisioned by that owner
(`on_behalf_of` matching). Without this, any generic caller could
create a blueprint that masquerades as module-owned — hidden from
the generic admin yet picked up by the owning module's listings
(panel finding, 2026-08-18 review).

# `validate_delete`

```elixir
@spec validate_delete(struct(), keyword()) :: :ok | {:error, term()}
```

Validates deleting `entity`. Owner-originated calls consult nothing;
generic calls are refused; the owner's registered guard arbitrates
owner-side deletes.

# `validate_mutation`

```elixir
@spec validate_mutation(struct(), map(), keyword()) ::
  :ok | {:error, :managed_blueprint | :locked_key}
```

Validates an update to `entity` with `attrs`. Returns `:ok` or
`{:error, reason}`. Owner-originated calls (`on_behalf_of` matching
the owner) pass unconditionally.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
