monetize-embed — what a product compiles in. One struct, one question: does this tenant get to do this, right now? Answered from verified facts held in memory, in nanoseconds, with monetize down or not.
monetize-server ──signed Snapshot file──▶ EntitlementCache::refresh (start, catch-up)
monetize-server ──signed EntitlementFact─▶ EntitlementCache::push (every change)
product choke point ──────────────────▶ EntitlementCache::allows (every request)
The ladder table, as the product sees it
| state | Write (push, LFS upload) | Read (owner clone/fetch) | AnonymousRead | Admin (UI) |
|---|---|---|---|---|
| Free, Paid | allow | allow | allow (product still applies Public) | allow |
| Grace | allow, [EntitlementCache::notice] has the warning line |
allow | allow | allow |
| Suspended | refuse, named reason + URL | allow | refuse | refuse: pay page only |
| Retention | refuse | allow (export) | refuse | refuse: pay page + export |
Two rules, not negotiable: never hold data hostage — Read is allowed in every
state, a lapsed tenant can always clone their own repositories; and refuse before
the bytes — the product asks at authorise_service, before a pack is read, and
puts [Verdict::Refuse::reason_line] in report-status.
What is, and is not, decided here
- The
statein a fact is monetize's verdict at push time. The cache does not re-derive it as the clock moves — it does not know the policy's day counts, and the product contract says a product keeps serving on its cached entitlement when monetize is unreachable. Ladder transitions arrive as new pushes. - An unknown tenant is
Free: the product's default, never a refusal. - Visibility (
Public) and quotas (caps) are the product's own checks; this crate only says whether the entitlement allows the kind of action.
Cost of the read path
[EntitlementCache::allows] is one ArcSwap::load (an atomic increment on a
debt-slot, ~2 ns, no lock, no allocation), one BTreeMap lookup on the tenant name,
and a match. A refusal allocates its two strings; an allow allocates nothing. Writes
(push, refresh) clone the map and swap the pointer — O(tenants), taken by the
rare path on purpose so the hot path never contends.