revoltify/subscriptionify
Subscriptionify is a Laravel package for adding subscription plans and feature access to your app. Define plans, features, and usage limits, subscribe Eloquent models, and check access through a clean, flexible API.
Install the package with Composer, publish its migrations/config, and add the package's subscription trait to every model that should own entitlements (typically User, plus any Team/Workspace/Project models you bill against). Run migrations before writing any feature checks — grants and plan features are persisted, not computed from config.
Where to look first:
subscriptionInfo() trap.First use case — gate a feature behind a grant, no plan involved:
$user->grantFeature('beta-dashboard');
$user->featureInfo('beta-dashboard'); // works: resolves the grant directly
$user->allFeatures(); // 1.0.2+: includes the grant
Then check it at the boundary (middleware, policy, or Blade directive) rather than scattering featureInfo() calls through controllers.
Plan features for the normal path, grants for the exceptions. Keep your priced tiers in subscription plans and reserve grantFeature() for things a plan can't express: comped accounts, beta flags, one-off purchases, per-record overrides. Since 1.0.2 these two sources are unioned in allFeatures(), so you no longer need a parallel "grants only" code path in your UI.
Enumerate with allFeatures(), inspect with featureInfo(). featureInfo($slug) answers "what are the limits for this one feature" and has always resolved grants correctly. allFeatures() answers "what does this subscribable have" and is what you want for pricing tables, admin screens, usage dashboards, and bulk serialization. Before 1.0.2 these two disagreed; use allFeatures() as the source of truth for enumeration and the disagreement disappears.
Mirror a parent's entitlements onto children using the documented recipe: read allFeatures() off the parent, then grant the relevant slugs on the child. Do not reach for subscriptionInfo() here — it returns an empty DTO when the child has no subscription of its own, which is exactly the case the recipe is for.
Let overlap do its job. When a plan and a grant name the same slug, the package resolves it for you: quota is additive, while unit price and reset cycle are overrides. That means a +50 grant on top of a 100 plan feature yields 150, but the grant's unit price (if set) wins rather than summing. Model top-up purchases as grants and let the package combine them; don't compute the total yourself.
Version your serialized DTOs. subscriptionInfo() now delegates to allFeatures(), so its features collection can contain entries it previously omitted. If you return that DTO from a controller or API resource, treat this as a payload change: bump an API version, or map the DTO into your own resource class so package changes don't leak into your contract.
Test at the grant/plan boundary, not just the happy path. The bugs this release fixed were both "works in isolation, wrong in combination." Cover: grant-only subscribable, plan-only subscribable, overlapping plan + grant, and no subscription at all.
subscriptionInfo() is plan- and billing-shaped. It still returns an empty DTO for a subscribable with no subscription — that is intentional, not a regression. If your model is grant-only, calling it gives you a hollow object that looks like a bug. Use allFeatures() there.
1.0.2 silently widens serialized output. Anything that snapshots, caches, or asserts on the features collection from subscriptionInfo() will change. Audit JSON snapshot tests and any cached entitlement payloads after upgrading; a stale cache entry is indistinguishable from a permissions bug.
Don't sum quotas by hand. Overlapping slugs are deduplicated in allFeatures() — listed once, with the additive limit already applied. If you iterate the collection and add up limits you'll be fine; if you separately query the plan and the grant and add those, you'll double-count in some paths and miss overrides in others.
Ordering is plan-first. Plan ordering is preserved and grant-only slugs are appended. Don't assume alphabetical or insertion order when rendering a feature list; sort explicitly at the presentation layer if order matters.
Overrides are easy to misread. A grant that sets a reset cycle changes the cycle for the combined feature rather than adding a second schedule. If a customer expects "both cycles," that's not what the package models — express it as two distinct slugs.
Invalidate caches on grant changes. grantFeature() and any revoke path mutate persisted state; if you cache allFeatures() output (reasonable for a pricing page), key the cache to the subscribable and bust it on grant/revoke, not just on subscription changes.
Debug from the builder. When a feature is missing or has the wrong limit, drop to FeatureInfoBuilder::buildAll() and inspect what the union produces for that subscribable before blaming your policy layer. It's the single chokepoint both allFeatures() and subscriptionInfo() now share, so a wrong answer there explains every downstream symptom.
Extension point: wrap, don't fork. Shape output for your frontend in your own API resource around the DTO. Overriding the builder couples you to internal resolution rules that just changed once and will change again.
How can I help you explore Laravel packages today?