flarum/subscriptions
Flarum Subscriptions lets users follow discussions and tags, receive notifications about new activity, and manage their subscriptions from a dedicated page. It adds flexible subscription controls to help communities keep members engaged with relevant content.
flarum/subscriptions is the bundled Flarum extension that adds Stripe-backed paid subscriptions and ties them to group membership. The repository is a read-only subtree split of flarum/framework; do not open issues or PRs here — everything goes to the monorepo.
composer show flarum/subscriptions
# if missing:
composer require flarum/subscriptions
/admin#/extensions → Subscriptions, then clear cache (php flarum cache:clear).stripe listen --forward-to <url>).viewDiscussions / startDiscussion. That is the whole first use case: a paid members-only category, with zero custom code.Where to look first
src/SubscriptionPlan.php, src/Subscription.php — the two models that matter.src/Api/ — SubscriptionPlanResource, SubscriptionResource serializers.src/Listeners/ (or src/Webhook/) — Stripe webhook handling; this is where group sync happens.migrations/ — table shapes before you add columns.js/src/forum/ — plan list, checkout flow, user subscription menu; js/src/admin/ — plan CRUD.Gate by group, not by subscription record. Access checks should stay in Flarum permissions ($actor->hasPermission('tag5.viewDiscussions')), because the extension's job is to keep users_groups in sync with Stripe state. Building your own Subscription::where(...) checks duplicates that logic and drifts.
Expose extra plan data to the frontend. Add a migration on subscription_plans, then extend the serializer:
new Extend\ApiSerializer(SubscriptionPlanResource::class)(function ($serializer, $plan, $attributes) {
$attributes['badgeColor'] = $plan->badge_color;
})
Remember to add the column to the model's $casts/accessors and to the admin form component in js/src/admin.
Grant comped subscriptions deliberately. Creating a Subscription row alone does not add the user to the group; group membership is what grants access. Either add the user to the group directly (and accept that Stripe has no record), or create a Stripe subscription with a 100%-off coupon so webhooks keep it consistent. Pick one and document it — mixed approaches produce users who lose access on the next webhook.
Reconcile with a scheduled command. Webhooks get lost. A nightly job that pulls active subscriptions from Stripe and diffs against local subscriptions rows (and group membership) catches nearly all support tickets:
Schedule::command('subscriptions:reconcile')->daily();
React to lifecycle changes in listeners. Listen for the extension's subscription events (check src/ for the exact event classes in your version) or, more robustly, hook your own listener onto the webhook handler output. Keep listeners queued — webhook responses must be fast or Stripe retries.
Frontend integration. Wrap existing components rather than replacing them:
extend(IndexPage.prototype, 'sidebarItems', (items) => {
if (!app.session.user?.groups.some(g => g.id() === SUB_GROUP_ID)) return items;
items.add('member-badge', <span className="badge">Supporter</span>, 5);
});
Note that app.session.user.groups is stale until reload after a webhook grants access.
Local testing loop. Stripe CLI forwards events; use stripe trigger customer.subscription.updated and test clocks for renewal/cancellation paths. Verify group add on checkout.session.completed, group removal on customer.subscription.deleted, and no removal on cancel_at_period_end.
flarum/framework; local fixes belong in your own extension.settings table or via env requires php flarum cache:clear, otherwise the old key keeps validating webhooks.past_due and unpaid states are the fuzzy middle. Decide explicitly whether they keep access; the default webhook handling in your version may differ from expectations. Test with Stripe test clocks rather than waiting a month.$user->unsetRelation('groups') (or reload the actor) before checking permissions in the same request, or you'll evaluate against stale groups.Subscription casually. Its rows are keyed to Stripe IDs (stripe_id, stripe_status). Custom columns are fine; reinterpreting status values breaks the sync logic.flarum-subscriptions.forum.* and flarum-subscriptions.admin.*. Translation packs that predate your Flarum version may miss newer keys.fof/-namespaced) that also mutate group membership will fight this one. Pick a single source of truth for group assignment.How can I help you explore Laravel packages today?