Subscription Plans
Available since: brezel-spa@5.0.0
, brezel/api@3.0.0
Basic concept
Some things in a brezel system can be bought / unlocked with a subscription based paywall.
For that, there is the concept of “plans” which can be bought/activated via a subscription. These define prices and most importantly, “Features” that can be used to enable or disable things in the system.
Concept Summary
- A
User
entity with the “Billable” Capability can buy a plan for anEntity
. - The
User
will be registered as a customer in stripe (identified by their email) and charged for the selected plan by stripe. - Only entities from
Modules
that use theBuyable
Capability can be bought. In the options of that capability, thesubscription_plans
, which contain feature flags and prices, are defined. During bakery apply, configured plans will be created in Stripe as Products with their respective prices. - Any
Entity
from aBuyable
Module will have the fieldsactive_plans
andactive_features
set, which can be used by workflows, policies, recipes etc. to enable or disable features.- You should only use the
active_features
field to check for features, as they are unique and derived from theactive_plans
field.-
Tip: You can use
entity.hasActiveFeature(<feature_identifier>)
in both API and SPA recipes to check if an entity has a specific feature active.
-
- You should only use the
- To react on payment events, there are several workflow events that can be used to hook into.
Additional features to manage plans
- Ability to build an overview that see all active plans via routes / SPA Api helpers
/modules/<module_identifier>/resources/<entity_id>/subscriptions
viaApi.getSubscriptionsForEntity(entity: Entity)
/modules/<module_identifier>/subscriptions
viaApi.getSubscriptionsForModule(module: Module)
- These endpoints return a structure like this:
[{"id": 1, // Unique identifier for the subscription"user_id": 1, // The ID of the user that bought the plan"type": "ultra", // The identifier of the plan that was bought"stripe_id": "sub_1RiuvmBFAS1bkUxqN0ae5IzV", // The ID of the subscription in Stripe"stripe_status": "active", // The status of the subscription in Stripe"module_id": 2, // The ID of the module if the entity that the subscription is for"resource_id": 1, // The ID of the entity that the subscription is for"stripe_price": "price_1RiXrVBFAS1bkUxqrYZ4fnXt", // The ID of the price in Stripe that was bought"quantity": 1, // Always 1, as we only support single subscriptions for now"trial_ends_at": null, // The date when the trial ends. Always null since we don't support trials via stripe."ends_at": null, // When the subscription ends. Set to the date when the subscription will be deactivated, once the subscription was canceled"next_billing_at": "2026-07-09 10:08:50", // The date when the next payment will be charged. This is the date when the subscription will be renewed."created_at": "2025-07-09T10:08:56.000000Z", // The date when the subscription was created / first activated"updated_at": "2025-07-09T10:08:56.000000Z", // The date when the subscription was last updated"items": [{}] // An array of the `subscription_items` of that subscription. Should always just contain one item. You can get the stripe id (e.g. `prod_xxxxxxx`) of the Stripe product via `items[0].stripe_product`}]
- Ability to manually activate/remove plans to an entity
active_plans
is just a textarea withmode = tags
. You can add (or remove) another identifier freely, and upon saving, the system will update theactive_features
field accordingly.- You can also activate subscriptions via the Stripe dashboard if you indeed want them to be payed for.
- Prerequisites:
- Have customers created in Stripe
- Have at least one working payment method for that customer
- Have the plans and prices your Brezel expects created in Stripe via bakery apply
- How do do it:
- Navigate to https://dashboard.stripe.com/test/customers/{customer_id}
- Click on “Add subscription”
- Choose the right plan and price as product
- The subscription will start immediately, but if you want to bill the user later (e.g. useful when migrating from the old Stripe integration) set the “Ab abrechnen” field accordingly.
- Set the correct metadata. For a minimum you need these fields:
module_identifier
: The identifier of the module that the entity is from (e.g.buyable_thingy
)resource_id
: The ID of the entity that the subscription is for (e.g.1234
)plan_identifier
: The identifier of the plan that was bought (e.g.pro_monthly
)
- Choose the right payment method
- Add a meaningful name (which will be shown in the billing portal) as a description. E.g.
"Pro Plan - Buyable Thingy 1234"
- Click on “Create subscription”
- Now Stripe will send a webhook to your Brezel which will act like a normal Subscription activation.
- Afterward you should have the plan with features active on the desired entity, and the user should be able to open the billing portal.
- Prerequisites:
- Ability to start a Stripe customer billing portal session for a user to manage their subscriptions
- All subscription cancellations / adjustments are handled via the stripe customer portal. A link to which can be generated for the current user via the endpoint
/subscription-portal
- All subscription cancellations / adjustments are handled via the stripe customer portal. A link to which can be generated for the current user via the endpoint
Use cases
Buy something for an Entity
A User can buy a plan on an Entity.
Your business logic can then enable or disable functionality for things concerning that Entity based on that features are unlocked by the bought and activated plan.
Examples:
- Unlocking “premium” features for a website in KAB.page
- A user buys the “premium” plan for a website.
- The system enables the “premium” feature for that website.
- The KAB.page business logic looks at the active features for that website, sees that the “premium” feature is active and enables / unlocks e.g. deployment, custom CSS and other things.
- Unlocking “shop” features for a website in KAB.page
- A user buys the “shop” plan for a website.
- The system enables the “shop” features for that website.
Buy something for a Client
A User can buy a plan for a Client (which is just the same as any entity).
Your business logic can then enable or disable functionality for things concerning all Entities from that client based on that features are unlocked by the bought and activated plan.
That could include all Users and Entities of that Client.
Remember, this has to be handled by custom business logic, the system itself only activates feature flags!
Examples:
- Enabling client wide features
- The user buys the plan to use feature X for his client
- The system enables feature X for all Users and Entities of that Client.
- e.g. allowing access to a new module / feature-set
- SAAS style plans
- A user buys a “business” plan for a Client.
- The system enables the “business” features for that Client.
- That allows X users to be created for that client
- Enables X feature (e.g. DATEV export) for bills of that client
- The system disables the “business” features for all Users and Entities of that Client when the plan expires.
- All users except the main user that bought the plan (hopefully the boss) are disabled and cannot log in anymore.
- Or just disable the creation of any new entities for that client.
- Unlocking the ability to use KAB.instructions for a specific amount of usages
- A user buys the “instructions_100” plan on the client
- The plan is activated on the client, because the custom business logic is not exclusively happening on the “bought” entity, but multiple entities from that client are relevant to how the feature works.
- The system enables the “instructions_100” feature for that client.
- This allows the client handle up to 100 instruction uses per month.
- Custom business logic would create and maintain a counter on the client object wich resets to 100 at the beginning of each month.
- Every time an instruction is used, the counter is decremented.
- If the counter reaches 0, custom business logic would not allow new instruction usages until the reset next month.
- A user buys the “instructions_100” plan on the client
How to react to payment events?
There are several workflow events that can be used to react to payment events.
Some are to be used for feature management and day-to-day operations.
These all return the Entity
in question and either the identifier of the plan or the list of NOW active_features.
event/subscriptionActivated
: Fires when a plan was activated for an entity of the specifiedmodule
.event/subscriptionDeactivated
: Fires when a plan was deactivated (e.g. when a subscription runs out) for an entity of the specifiedmodule
.event/subscriptionPaymentFailed
: Fires when a payment for a plan failed for an entity of the specifiedmodule
.event/subscriptionFeaturesChanged
: Fires when theactive_features
field on an entity of the specifiedmodule
was updated.- This is useful to
- react to a newly activated plan, e.g. when a user buys a plan for an entity.
- react to a deactivated plan, e.g. when a user cancels a plan for an entity.
- react to changes in the features of a plan, e.g. when a feature was added or removed from a plan via the config.
- This is useful to
event/subscriptionInvoicePaid
: Fires when a payment for a plan was successful. Includes the Entity and the relevant Plan / Price definitions
Remember: Stripe already sends out emails to the user! Only send additional emails if you want to notify the user about something that is not covered by the stripe emails.
Architecture overview
Buyable
Type definitions on module level:
type Plan = { id: string; // Unique identifier for that plan. Used as `lookup_key` for stripe product and as identifier to use when checking for features. name: string; // Human readable name of the plan and name of the product in stripe. features: Feature[]; // Features of the plan. The same feature can be used in multiple plans. price: Price[]; // Prices of the plan.};
type Feature = { identifier: string; // Unique identifier for the feature. Can be used to enable or disable the feature in the system. name: string; // Human readable name of the feature.};
type Price = { identifier: string; // Unique identifier for the price. Used as `lookup_key` for stripe price. price: number; // Price with 2 decimal places. E.g. 9.99 currency: string; // Currency to use in stripe. 3 letter ISO Currency code E.g. "EUR", "USD", "INR" interval: "day"| "week" | "month" | "year"; // Payment interval to be used for the subscription.};
Fields present on an Entity that could have active plans (i.e. from a Module with the Buyable
capability):
type Entity = { active_plans: string; // Comma seperated list of active plans for that entity. e.g. "pro_monthly, lite" active_features: string; // Comma seperated list of active features of that entity. e.g. "pro_feature_x, pro_feature_y, lite_feature_x". This is derived from all features of all active plans, and unique.
// ... All other fields of the entity, including id, module_id etc.};
Billable
Field requirements on entity that should be able to buy something:
[ { identifier: "name", // Name of the user to use in stripe type: "text", options: { rules: "required", // This is required for stripe taxes to work }, }, { identifier: "email", // Email to register the user in stripe with type: "email", options: { rules: "required", // This is required for stripe taxes to work }, }, { identifier: "country_code", // ISO 3166-1 alpha-2 country code of the user type: "text", options: { rules: "required", // This is required for stripe taxes to work }, }, { identifier: "postal_code", // Postal code of the user type: "text", options: { rules: "required", // This is required for stripe taxes to work }, },]
Usage
- Set up the required Stripe configuration
- Set up a Stripe account and get your API keys. Fill
STRIPE_KEY
,STRIPE_SECRET
andSTRIPE_WEBHOOK_SECRET
in your.env
file. - Take a look and do the required configurations do be done in the stripe dashboard
- Set up a Stripe account and get your API keys. Fill
- Add the
Billable
capability to a User module. - Define a module with the
Buyable
capability. - Configure the available
subscription_plans
in the options of that capability. - Apply those changes via the bakery planner.
- This will create the products and prices in Stripe and get everything ready for the user to buy a plan.
- Make the user buy a plan for an entity.
- Either using the default integration (a button in the detail view of any Buyable entity), or build a custom widget that uses the provided Api functions to initiate a subscription.
- Use the
active_features
fields on the entity to enable or disable features in your workflows, policies, recipes etc.- Specifically, use the
entity.hasActiveFeature(<feature_identifier>)
function to check if a feature is active for that entity.
- Specifically, use the
A full example is available in the Brezel Playground
A full-context example of how to configure a system can be found here: basedOnBrezel/brezel-playground:feat/subscriptions
It contains a module things
which can be bought by users
.
Hint: use the default “plans” button shown in the button bar of the show view of a “things” entity to buy a plan for it / navigate to billing portal.
If premium_feature_1 is active on a “thing”, it will display a headline in the detail view of it.
Example configuration of a user
This defines a user with the Billable
capability.
It uses all the default fields (will create them in this case) except for the name
field, which is mapped to the existing full_name
field.
JSON Configuration for a user
{ "resource_user": { "identifier": "user", "type": "user", "options": { "capabilities": [ { "identifier": "Billable", "options": { "field_mapping": { "name": "full_name" } } } ] }, "fields": [ { "identifier": "full_name", "type": "text" } ] }}
Example configuration of a module
This defines two plans with the same features payable either monthly and yearly either in $ or €.
Includes another “lite” plan with only one of the features.
There also is an “unreleased” plan that is not available for purchase, but can be used to enable features that are not yet released.
That can be done by manually adding unreleased
to the active_plans
field of an entity and saving it.
JSON Configuration for a module
{ "resource_module": "buyable_thingy", "resource": { "identifier": "buyable_thingy", "type": "module", "options": { "capabilities": [ { "identifier": "Buyable", "options": { "subscription_plans": [ { "identifier": "pro_monthly", "name": "Pro subscription monthly", "description": "This gives you cool PRO features", "features": [ { "identifier": "pro_feature_x", "name": "Feature X" }, { "identifier": "pro_feature_y", "name": "Feature Y" } ], "prices": [ { "identifier": "pro_monthly_usd", "price": "21.00", "currency": "usd", "interval": "month" }, { "identifier": "pro_monthly_eur", "price": "20.00", "currency": "eur", "interval": "month" } ] }, { "identifier": "pro_yearly", "name": "Pro subscription yearly", "description": "This gives you cool PRO features", "features": [ { "identifier": "pro_feature_x", "name": "Feature X" }, { "identifier": "pro_feature_y", "name": "Feature Y" } ], "prices": [ { "identifier": "pro_yearly_usd", "price": "180.00", "currency": "usd", "interval": "year" }, { "identifier": "pro_yearly_eur", "price": "180.00", "currency": "eur", "interval": "year" } ] }, { "identifier": "lite", "name": "Lite subscription", "description": "Get a taste of some PRO features", "features": [ { "identifier": "pro_feature_x", "name": "Feature X" } ], "prices": [ { "identifier": "pro_monthly_usd", "price": "16.00", "currency": "usd", "interval": "month" }, { "identifier": "pro_monthly_eur", "price": "15.00", "currency": "eur", "interval": "month" } ] }, { "identifier": "unreleased", "name": "Unreleased plan", "description": "This is a plan is for features that are not yet released. It can not be bought.", "features": [ { "identifier": "unreleased_feature_x", "name": "Unreleased Feature X" } ] } ] } } ] } }}
Configurations in the stripe dashboard
Automatic emails
Configure automatic emails in the stripe dashboard to send emails to users when their payment fails, their card expires or they need to confirm a payment.
To do that, navigate to the stripe automatic billing settings.
Then set the following options:
Handling payment failures
If payments fail, you want them to be retried automatically and not just canceled immediately. However, if all retries fail, the subscription should be canceled at which point your application will react to that as if the plan was deactivated.
To do that, navigate to the billing settings and set the following options:
Technical implementation details
- Entities from Modules with the
Buyable
capability can be bought by users with theBillable
capability. - The plans (products in stripe) and their prices defined in the
Buyable
Capability are automatically created and synced with Stripe at bakery apply time.- Uses the
identifier
aslookup_key
for the price in stripe as this is the “identifying information” we use when buying a plan - The identifier of the plan is set as the metadata key
plan_identifier
on the stripe product. - Combination of price and plan is validated to prevent a user from buying a “higher value” plan with a lower price from another plan
- Uses the
- The
Billable
capability requires the following fields to exist and be filled on the user Entity:email: email
: The email of the user. Should already be there as it is a default field.name: text
: The name of the user in stripe.country_code: text
: ISO 3166-1 alpha-2 country code the user is located in.postal_code: text
: The postal code of the user.
- The fields on the
Billable
capability can be mapped to re-use existing fields in the options of the capability.- This is done via the
field_mapping
option.- The keys of this object are on of the field identifiers above, and the value is the identifier of the (existing!) field to use.
- This is done via the
- The
active_features
andactive_plans
fields on a Buyable Entity are- statically stored list of “slugs” (the identifiers of all features (unique) from all plans the entity has and the identifiers of all plans of an entity respectively).
- e.g.
active_features: "pro_feature_x, pro_feature_y, lite_feature_x"
oractive_plans: "pro_monthly, lite"
- e.g.
- updated when the important events (that also trigger the workflow events) are fired.
- When the active plans changed
- i.e. new plan was bought or an existing plan was canceled / deleted
- When the underlying plan changed
- i.e. adding or removing features from a plan
- When the active plans changed
active_features
is a list of all “feature flags” that are enabled for that entity.- They are derived from the features of all active plans and are a unique list of comma separated feature identifiers as configured.
- You should always use
entity.hasActiveFeature(<feature_identifier>)
to check if a feature is active for an entity, but manually looking at theactive_features
field is also possible.
- statically stored list of “slugs” (the identifiers of all features (unique) from all plans the entity has and the identifiers of all plans of an entity respectively).
- When existing plans are DELETED i.e. removed from the config, the following happens:
- No changes in Stripe will occur.
- The products and prices will not be deleted or archived.
- Active subscriptions will not be canceled.
- Entities that had the plan active, will have the respective features removed.
- The plan identifier will still be present in the
active_plans
field, but the features will be removed from theactive_features
field!
- The plan identifier will still be present in the
- Re-adding the plan will restore features and the Stripe integration will work as if nothing ever happened.
- No changes in Stripe will occur.
Use Cases
Technical step, by step detailing of common use cases.
Buying a plan
- Request to
/modules/<module_identifier>/resources/<entity_id>/subscribe/<plan_identifier>/<price_identifier>
with payload:OR just use{success_url: "<SPA URL to current entity detail view>", // e.g. /modules/buyable_thingy/1234#subscription-initiatedcancel_url: "<SPA URL to current entity detail view>", // e.g. /modules/buyable_thingy/1234#subscription-failed}Api.getCheckoutSessionURLForNewSubscription(entityID: number, moduleIdentifier: string, planIdentifier: string, priceIdentifier: string)
- This will extract the user from the request and construct a URL for a Stripe checkout session.
- Redirect the user to the returned URL.
- This will load a stripe checkout page for the selected plan and price for the given entity.
- Email, name and tax info (country and postal code) will be prefilled from the
Billable
user entity.
- When finished, User will be redirected by stripe back to the URLs provided (e.g.
/modules/buyable_thingy/1234#subscription-initiated
).- The added parameter can be captured by widgets on the detail view to show subscription status by looking at the new values of
active_features
(oractive_plans
).
- The added parameter can be captured by widgets on the detail view to show subscription status by looking at the new values of
Canceling a subscription, checking active subscriptions, changing payment method etc.
- Request to
/modules/<module_identifier>/resources/<entity_id>/subscribtion-manager
OR just useApi.getBillingPortalURL()
- Redirect the user to the returned URL wich will load the stripe customer portal.
If cancelling something, the application will handle this via the webhooks.
Endpoints to get infomation about plans
/modules/<module-identifier>/subscriptionPlans
: Returns all “buyable” subscription plans (i.e. ones with at least one price defined) for the module with identifiers, features and prices./modules/<module-identifier>/allSubscriptionPlans
: Returns all configured subscription plans for the module, even ones that don’t have any prices (i.e. private plans that can be used to unlock features manually).
Status / Roadmap
Click to expand
- Writeup concepts of a plan
- Remove old license and subscription code
- brezel/api
- brezel/spa
- Enable Capabilities to take options
- Enable Capabilities to require even existing fields (
rules required
, not just that the field exists at all)
- Enable Capabilities to require even existing fields (
- Implement Capabilities
- Billable
- Required fields
- Buyable
- Required fields
-
active_features
-
active_plans
-
- Pass an entity and set all active plans and features
- Required fields
- Billable
- Integrate Laravel Cashier
- Handle webhooks
- Handle product / price creation via bakery planner from the capability
- create products and prices fresh
- handle existing products / prices
- products are looked up by metadata key and updated if changed
- prices are looked up by lookup_key and if changed, old one is archived and new one is created only when changed
- if prices are removed (i.e. present on the product but NOT defined by us), archive them. But only if they came from us in the first place (i.e. metadata key
managed_by_brezel
is set`)
- build subscription route
- build subscription manager (customer portal) route
- handle taxes
- sync customer data to stripe on Billable user update
- Make the title (
brezel_name
) of the bought entity viewable in the billing dashboard to identify what subscription is for which entity (i.e. description of the subscription) - ensure stripe has a customer portal set up because we lean heavily on it for subscription management
- Build an endpoint to get subscription date and remaining time for a plan of an entity
- Store relation to module and entity in the Subscription table
- Store date of next payment in the Subscription table
- Build relation between Subscription and Entity, to get all Subscriptions for an Entity
- Build route to get all Subscriptions for an Entity
/modules/<module_identifier>/resources/<entity_id>/subscriptions
- Build route to get all Subscriptions for a Module
/modules/<module_identifier>/subscriptions
- Add additional metadata key
module_id
alongside themodule_identifier
metadata- Refactor all the matching logic to use the
module_id
instead of themodule_identifier
metadata key. Just for robustness reasons, works with the identifier as well, but this would be more robust to e.g. survive module renames.
- Refactor all the matching logic to use the
- Create endpoint to list all available plans with prices for a module
- Automatically create stripe webhook for the current system if needed
- Configure stripe automatic emails
- Has to be configured in stripe dashboard https://dashboard.stripe.com/settings/billing/automatic.
- Added dock block above to remind users to do this.
- Throw and build workflow events
-
event/subscriptionActivated
: Fires when a plan was activated for an entity of the specifiedmodule
.- Triggered whenever a plan was activated for an entity.
- Triggered directly by stripe subscription.created webhook
- Triggered when a plan was manually activated on an entity
- Triggered whenever a plan was activated for an entity.
-
event/subscriptionDeactivated
: Fires when a plan was deactivated (e.g. when a subscription runs out) for an entity of the specifiedmodule
.- Triggered directly by stripe subscription.deleted webhook
- Triggered when a plan was manually deactivated on an entity
-
event/subscriptionPaymentFailed
: Fires when a payment for a plan failed for an entity of the specifiedmodule
.- Triggered directly by stripe invoice.payment_failed webhook
- You probably should not really react to this, as a properly configured stripe will send an email to the user and the user can react to it. This might just be helpful for internal statistics or marking the entity as “payment failed” in the system.
- Triggered directly by stripe invoice.payment_failed webhook
-
event/subscriptionFeaturesChanged
: Fires when theactive_features
field on an entity of the specifiedmodule
was updated.- Triggered if something changed in the
active_features
field of an entity.- On subscription activated / deactivated
- On manual active_plans change
- When configured features of a plan change
- Triggered if something changed in the
-
event/subscriptionInvoicePaid
: Fires when a subscription invoice was paid.- Triggered directly by stripe invoice.payment_succeeded webhook
- Contains the
Entity
and the Plan and Price details the invoice was paid with (i.e. price config)
- Contains the
- Triggered directly by stripe invoice.payment_succeeded webhook
-
- Extend
event/webhook
with feature flag filtering- i.e. only allow the event if the entity it was triggered for has the required feature flag active.
- Handle feature flags / subscription events
- Every time a subscription is activated/deactivated
- set
active_features
on the right entity (based on the metadata of the subscription) - set
active_plans
on the right entity (based on the metadata of the subscription)
- set
- When the features of a plan changed, update
active_features
for all entities that have that plan active- We sync! I.e. if a feature was added, give it to everyone that has that plan, if a feature was removed, remove it from everyone that has that plan.
- Handle removing plans form the config
- Keep the plan identifier in
active_plans
, but remove the features since we don’t know them anymore. - We do not delete plan (products) or prices in stripe
- Keep the plan identifier in
- Handle manually activated plans
- If a defined plan has no prices, don’t return it when fetching available plans
- update
active_features
when manually changingactive_plans
- Make sure that when rebuilding the
active_plans
field on a subscription change, only add / remove the plan that this subscription was for. Leave the others untouched.- I.e. when I have
plan_a, plan_b
and I buyplan_c
, the new value ofactive_plans
should beplan_a, plan_b, plan_c
- Now I manually add
plan_d
to theactive_plans
field, the new value should beplan_a, plan_b, plan_c, plan_d
- When
plan_a
is deactivated, the new value should beplan_b, plan_c, plan_d
, even thoughplan_d
was not bought via a subscription.
- I.e. when I have
- Every time a subscription is activated/deactivated
- Build migration
Maybe migrate over old data fromentity_subscription
table to the new way of storing / registering subscriptions- Remove old tables and columns
- license stuff
license_plan_module
tablelicense_plans
tablelicenses
table- All fields, need to find them
- old subscription stuff
module_subscription
tablemodule_subscription_prices
tableentity_subscription
table- All fields, need to find them
- license stuff
- Dump old tables and data
- Build some default SPA handling for all of this
- Widget / Modal for each entity from a Buyable module that
- Shows the active plans and features
- Allows to buy a plan
- Allows to manage the subscription (redirect to stripe customer portal)
- Api class helper functions to
- Get all buyable plans for a module
- initiate a subscription for a plan on an entity with a specified price
- get all plans, not just the ones with prices
- Rebuild how translations are handled for the plans.
- Remove the
name
ordescription
fields of plans, features and prices. They should be translated via their identifier.- Translation paths are
modules.<module_identifier>.subscription_plans.<plan_identifier>.title
,modules.<module_identifier>.subscription_plans.<plan_identifier>.description
modules.<module_identifier>.subscription_plans.features.<feature_identifier>
modules.<module_identifier>.subscription_plans.prices.<price_identifier>
- Translation paths are
- Remove the
- Widget / Modal for each entity from a Buyable module that
-
entity.hasActiveFeature(<slug>)
- Api recipes
- SPA recipes