# Entitlement Model (/docs/concepts/entitlements)



Ductor is multi-tenant from the ground up. Every pool, rule, connection, and
workflow run is scoped to a tenant, and **entitlements** decide what each tenant
is allowed to do and how much of it. Entitlements sit *on top of*
[tenant isolation](/docs/concepts/tenancy) — isolation is what makes a limit
meaningful, entitlements are the limits themselves.

## Plans and quotas [#plans-and-quotas]

Entitlements combine two ideas:

* **Plan** — the set of features and ceilings a tenant is provisioned for
  (integrates with Stripe billing where enabled). For how plan tiers are
  populated from Stripe billing and enforced, see
  [Billing & Usage → Plans & entitlement enforcement](/docs/billing/plans) and
  the [Stripe integration](/docs/billing/stripe).
* **Quota** — the runtime budget a tenant may consume: request volume, capacity,
  concurrency, and similar counters.

When a tenant exceeds what it is entitled to, Ductor fails **visibly** rather
than silently degrading. Quota exhaustion surfaces as a domain error
(`ErrQuotaExceeded`, wrapped by `QuotaError`) that the transport layer maps to a
precise HTTP / Connect status — so clients get a `429`-style signal they can act
on, not a mysterious empty result.

<Callout title="Fail visibly, never silently">
  This mirrors Ductor's core principle: every path either works correctly or
  fails observably. An over-quota tenant gets a clear, typed error — never a
  dropped request.
</Callout>

## Enforcement on the hot path [#enforcement-on-the-hot-path]

Entitlement checks run where the load is. Capacity is tracked with atomic Redis
Lua increments and decrements, so concurrency and rate ceilings are enforced
consistently across every pod without a central lock. Because the check is
cheap and colocated with routing, entitlements gate the [routing
pipeline](/docs/concepts/routing-pipeline) without adding a round-trip.

The same atomic-counter approach appears in the
[tiered queue](/docs/concepts/tiered-queue), where per-tenant and per-pool
concurrency limits are enforced at lease time. Whether work arrives through a
synchronous routing call or an asynchronous workflow step, the ceiling is the
same and it's enforced the same way.

## Errors you'll see [#errors-youll-see]

| Error                             | Meaning                                            |
| --------------------------------- | -------------------------------------------------- |
| `ErrQuotaExceeded` / `QuotaError` | The tenant is over its runtime budget.             |
| `ErrExhausted` / `ExhaustedError` | A capacity-limited target has no headroom.         |
| `ErrConflict` / `StateError`      | The requested change conflicts with current state. |

All of these are declared in `domain/errors/` and mapped to protocol codes in
the transport layer, so behavior is identical whether a client speaks REST or
Connect-RPC. Because the errors are typed domain sentinels rather than ad-hoc
strings, a client can branch on them programmatically — an over-quota condition
is distinguishable from a state conflict without string matching.

## Enforcement postures [#enforcement-postures]

This page is the *what and why* of entitlements. The runtime **enforcement
posture** — how a missing, stale, or unavailable entitlement is handled (`report`
vs `strict` vs `off`), the unprovisioned bootstrap grace, and exactly which
surfaces are gated (workflow publish/schedule, strategy selection, queue
admission, connector execution) — is documented in
[Auth & Security → Entitlements](/docs/auth/entitlements).

## Where to go next [#where-to-go-next]

* [Auth & Security → Entitlements](/docs/auth/entitlements) — enforcement postures and the gated surfaces.
* [Tenancy](/docs/concepts/tenancy) — the isolation that entitlements build on.
* [The Routing Pipeline](/docs/concepts/routing-pipeline) — where quota gates the hot path.
* [The Tiered Fair Queue](/docs/concepts/tiered-queue) — concurrency limits on asynchronous work.
