Decision records (ADR)
This page collects the load‑bearing decisions behind the package. Each follows the
Problem → Decision → Consequences shape. Decisions specific to a subsystem are also linked from that
subsystem’s page.
Redemption
ADR-001 · Lock-free conditional UPDATE for the seat claim
Problem. Prevent over‑redemption under concurrency without serializing every redemption behind a
lock.
Decision. A single conditional UPDATE … WHERE current_uses < max_uses that also flips state in
the same statement is the only path that bumps current_uses.
Consequences. No held locks, no deadlocks, portable across pgsql / MySQL / SQLite; safety is a
property of the statement. A rare same‑account double‑insert is compensated by releaseSeat(). See
Atomic idempotent redemption.
ADR-002 · UNIQUE(code_id, redeemer_id) is the idempotency anchor
Problem. Make a replayed redemption a no‑op across workers and restarts.
Decision. A unique index on (code_id, redeemer_id) in the append‑only invite_redemptions
table.
Consequences. Idempotency is a database invariant, not code discipline (host rule R21). The loser
of a race catches the violation, releases its over‑counted seat, and returns the winner’s claim.
Tenancy & seams
ADR-003 · Tenant scope at the application layer
Problem. Isolate customers that may share a project_key / code string.
Decision. tenant_id on every table; composite uniques lead with it; cross‑tenant isolation is
the mandatory forTenant() scope, not a tenant‑keyed FK.
Consequences. Free single‑tenant operation via a default tenant; every query must be scoped (an
unscoped read is a bug). See Multi‑tenancy & host seams.
ADR-004 · Vendor-neutral seams (TenantResolver / Provisioner / InvitedAccount)
Problem. Ship a reusable engine without coupling to a specific User model, tenancy package, or
permission system.
Decision. Three interfaces with safe defaults; the host binds its own implementations.
spatie/laravel-permission, laravel/fortify, laravel/mcp are optional integrations.
Consequences. Plain Fortify/Breeze apps and a complex multi‑tenant host both work with the same
core. Project/team membership — the one genuinely host‑specific concern — is a host‑supplied
provisioner.
ADR-005 · GRANT-never-REVOKE provisioning, best-effort
Problem. An invite that grants access must not be a vector to remove access, and a provisioning
fault must not undo a committed redemption.
Decision. Provisioning is additive only (grant role, firstOrCreate membership) and best‑effort —
faults are swallowed and logged.
Consequences. Redemption commits independently of provisioning; access only ever rises. A failed
grant is observable in logs but never rolls back a claimed seat.
Anti‑abuse & privacy
ADR-006 · Fail-open, generic anti-abuse
Problem. A fraud gate must not become a denial‑of‑service for real users, nor a probing oracle for
attackers.
Decision. Fail‑open (a fault → none, never a block) and generic (the caller only learns
rate_limited).
Consequences. Seat safety is the atomic claim’s job; the gate is advisory. Signals are logged for
review but the tripped rule is never echoed. See Anti‑abuse scoring.
ADR-007 · Anonymize-in-place, preserve aggregates
Problem. GDPR erasure must remove PII without corrupting current_uses, funnel counts, or
K‑factor.
Decision. Never delete rows; overwrite PII columns in place. PII (ip / fingerprint / email) is
stored as a salted HMAC, never plaintext.
Consequences. Retention sweeps and right‑to‑be‑forgotten requests leave every aggregate intact.
See GDPR & data privacy.
Codes
ADR-008 · Crockford Base32 + normalization as identity
Problem. Human‑typed codes suffer from confusable glyphs (I/1, O/0) and casing/separator
noise.
Decision. A Crockford Base32 alphabet that omits I L O U; every code is persisted in its
CodeNormalizer canonical form so the generator and redeemer agree on identity. The generator refuses
an alphabet containing the confusables or duplicates.
Consequences. A user can type q7-k9-2mnp and redeem Q7K92MNP. Signed codes survive
normalization unchanged. See Invite codes.
ADR-009 · Tri-surface over one core
Problem. Different consumers want PHP, HTTP, or MCP access — without three diverging
implementations.
Decision. One set of services; thin controllers and thin MCP tools adapt input → core → output
(host rule R44).
Consequences. A capability change lands once and is reflected on all surfaces; the surfaces cannot
drift. See Architecture overview.
These ADRs are mirrored in the package’s CLAUDE.md invariants so an AI pair‑programmer inherits them
automatically — see the AI vibe‑coding pack note on the home page.