# `Accrue.Plug.RequireEntitlement`
[🔗](https://github.com/szTheory/accrue/blob/accrue-v1.5.1/lib/accrue/plug/require_entitlement.ex#L1)

A controller-pipeline guard that halts a request unless the resolved billable
is entitled to a `feature:` (or holds an active `plan:`).

This is the route-level gate a Phoenix developer reaches for first. It is a
thin transport adapter: `init/1` validates the opts (raising at compile time
on ambiguous intent), and `call/2` delegates the allow/deny decision to the
always-compiled `Accrue.Entitlements.Guard` engine. It is pure Plug — there is
NO controller-framework coupling, so it works in any Plug pipeline.

## Usage

Gate exactly ONE of a `feature:` or a `plan:`:

    plug Accrue.Plug.RequireEntitlement, feature: :api_access
    plug Accrue.Plug.RequireEntitlement, plan: :pro

The deny behavior is content-negotiated and host-overridable:

    # Custom HTTP status on the opaque deny body (default 403):
    plug Accrue.Plug.RequireEntitlement, feature: :api_access, status: 402

    # Redirect instead of an opaque body (the target MUST live OUTSIDE this
    # gated pipeline to avoid a redirect loop):
    plug Accrue.Plug.RequireEntitlement, plan: :pro, on_deny: {:redirect, "/pricing"}

    # Resolve the billable explicitly (1-arity fn over the conn). Defaults to
    # `conn.assigns.current_scope.user` then `conn.assigns.current_user`:
    plug Accrue.Plug.RequireEntitlement, feature: :api_access,
      billable: &MyAppWeb.current_org/1

Global defaults for `on_deny:` / `billable:` live under
`config :accrue, :entitlements`; a per-guard opt always wins.

The `require_feature/1` / `require_plan/1` macros in `Accrue.Router` are sugar
over the single-`feature:`/`plan:` form; reach for the explicit `plug` form
above when you need `status:` / `on_deny:` / `billable:` overrides.

## Security

The billable is resolved from **server-side assigns only** (the host
`billable:` fn or the default `current_scope.user` / `current_user` probe) —
NEVER from request params or headers. The `accept` header is read for content
negotiation only, never for authorization. The deny path is the fail-closed
default: a `nil` billable, a raising `billable:` fn, or any resolution
exception all collapse to a 403. The deny body is opaque — it never echoes the
gated feature / plan name (D-10).

---

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