v2.0.0 - Subject model: workspace subscriptions + user memberships
Subscriptions 2.0 generalizes the workspace-only 1.x lifecycle engine into two coexisting, never-crossing products on one payment/lifecycle engine: workspace subscriptions (tenant billing, unchanged for existing callers) and user memberships (a user's paid relationship to a single workspace — paywalls, member benefits, recurring content access).
⚠️ Upgrading from 1.x — the bridge is mandatory
A populated install cannot migrate directly from 1.3.1. Migration 006 refuses to run without the 1.4.0 preparation marker:
- Put subscription writes into a maintenance window.
- Install 1.4.0 and run
php glueful subscriptions:prepare-v2(idempotent). - Install 2.0.0 and run
php glueful migrate:run.
Fresh installs: subscriptions:plans:import-config is now a required install step — with an empty subscription_plans table every entitlement resolves empty (a loud default_plan_unresolvable diagnostic tells you exactly this).
Breaking changes
- Subject-model schema (migration
006). Every domain row carries the(tenant_uuid, subject_type, subject_uuid)triple;plan_uuidbecomes a NOT NULL reference; uniqueness is now one subscription per subject. A newsubscription_provider_event_receiptstable is the first claim authority for inbound provider events. - Direct
subscription_overridesinserts must supplysubject_type/subject_uuid— a 1.x-shaped insert silently matches nothing (a hand-written deny override would stop denying). Use the new supported writer:OverrideRepository::upsertForSubject()/deleteForSubject(). - DB-authoritative catalog. Config plans are seeds only — no runtime overlay; new config plans require
subscriptions:plans:import-config. Plan keys are immutable and scoped by(audience, owner_tenant_uuid); subscriptions reference immutableplan_uuids. - Projector rejection semantics. Deterministic rejections (
missing_subject,invalid_subject,plan_scope_mismatch,subject_mismatch) commit an auditable receipt;UnmappedProviderSubscriptionExceptionis retryable — webhook endpoints MUST let it surface as a retry-inducing response. Provider payloads are sanitized (closed allowlist) in both receipts and events, including historical rows during006. - Stricter read-path validation.
current('')and the…For()methods throwinvalid subjecton empty/incoherent identities where 1.x returnednull.
New
- Subject-aware core (
currentFor/startFor/changePlanFor/cancelFor/reconcileFor(Subject …)) with the full 1.x tenant API preserved as a supported facade — existing integrations keep working. - Memberships are enabled by binding a
SubjectResolverInterface— no new config keys. IncludesMemberEntitlementResolver+require_member_entitlementmiddleware (fail-closed; rate tiers remain tenant-only). - Owner-fenced, receipts-first provider projection with deterministic race handling (same-plan idempotent, different-plan conflict) proven against real PostgreSQL savepoints (env-gated
PostgresSavepointTest,SUBSCRIPTIONS_TEST_PG_*). - Strict payvia delivery lane (requires
glueful/payvia >= 2.4): subscription events ride payvia's strict tagged lane with at-least-once delivery and genuine retry on the checkout race. With payvia ≤ 2.3 delivery degrades to the fault-isolated bus — unmapped events are then permanently lost, not retried (upgrade payvia to get the guarantee). - Tenant lifecycle mechanics: tenant-table registration, system-context discipline, and a host-neutral
SubscriptionSubjectDataPurgerfor user- and workspace-scoped purges. - Console subject options (
--subject-type/--subject-uuid) acrosssubscriptions:show/set-plan/reconcile; scopedplans:*commands (--audience/--owner).
Compatibility
- PHP ^8.3 · glueful/framework ^1.57 · payvia and tenancy remain optional (
suggest); payvia ≥ 2.4 required only for the strict delivery guarantee. - The 1.x tenant service facade is not deprecated — it is the supported API for workspace billing.
Two notes before you publish: the body assumes payvia 2.4.0 is released first (the strict-lane bullet references it, and this repo's require-dev resolves ^2.4 from Packagist) — publish that release before or together with this one. And make sure the pushed v2.0.0 tag points at 26f6c52 (the local tag moved when we dropped the CI workflow; if you pushed an earlier tag, force-push the tag or re-tag remotely first).