Skip to content

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 an Entity.
  • 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 the Buyable Capability can be bought. In the options of that capability, the subscription_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 a Buyable Module will have the fields active_plans and active_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 the active_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.

  • 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 via Api.getSubscriptionsForEntity(entity: Entity)
    • /modules/<module_identifier>/subscriptions via Api.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 with mode = tags. You can add (or remove) another identifier freely, and upon saving, the system will update the active_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:
        1. Navigate to https://dashboard.stripe.com/test/customers/{customer_id}
        2. Click on “Add subscription”
        3. Choose the right plan and price as product
        4. 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.
        5. 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)
        6. Choose the right payment method
        7. Add a meaningful name (which will be shown in the billing portal) as a description. E.g. "Pro Plan - Buyable Thingy 1234"
        8. 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.
  • 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

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.

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 specified module.
  • event/subscriptionDeactivated: Fires when a plan was deactivated (e.g. when a subscription runs out) for an entity of the specified module.
  • event/subscriptionPaymentFailed: Fires when a payment for a plan failed for an entity of the specified module.
  • event/subscriptionFeaturesChanged: Fires when the active_features field on an entity of the specified module 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.
  • 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

  1. Set up the required Stripe configuration
  2. Add the Billable capability to a User module.
  3. Define a module with the Buyable capability.
  4. Configure the available subscription_plans in the options of that capability.
  5. 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.
  6. 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.
  7. 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.

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: auto_emails_1.png

auto_emails_2.png

auto_emails_3.png

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: img.png

Technical implementation details

  • Entities from Modules with the Buyable capability can be bought by users with the Billable 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 as lookup_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
  • 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.
  • The active_features and active_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" or active_plans: "pro_monthly, lite"
    • 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
    • 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 the active_features field is also possible.
  • 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 the active_features field!
    • Re-adding the plan will restore features and the Stripe integration will work as if nothing ever happened.

Use Cases

Technical step, by step detailing of common use cases.

Buying a plan

  1. Request to /modules/<module_identifier>/resources/<entity_id>/subscribe/<plan_identifier>/<price_identifier> with payload:
    {
    success_url: "<SPA URL to current entity detail view>", // e.g. /modules/buyable_thingy/1234#subscription-initiated
    cancel_url: "<SPA URL to current entity detail view>", // e.g. /modules/buyable_thingy/1234#subscription-failed
    }
    OR just use 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.
  2. 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.
  3. 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 (or active_plans).

Canceling a subscription, checking active subscriptions, changing payment method etc.

  1. Request to /modules/<module_identifier>/resources/<entity_id>/subscribtion-manager OR just use Api.getBillingPortalURL()
  2. 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)
  • Implement Capabilities
    • Billable
      • Required fields
    • Buyable
      • Required fields
        • active_features
        • active_plans
      • Pass an entity and set all active plans and features
  • 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 the module_identifier metadata
      • Refactor all the matching logic to use the module_id instead of the module_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.
  • 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
  • Throw and build workflow events
    • event/subscriptionActivated: Fires when a plan was activated for an entity of the specified module.
      • 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
    • event/subscriptionDeactivated: Fires when a plan was deactivated (e.g. when a subscription runs out) for an entity of the specified module.
      • 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 specified module.
      • 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.
    • event/subscriptionFeaturesChanged: Fires when the active_features field on an entity of the specified module 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
    • 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)
  • 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)
    • 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
    • Handle manually activated plans
      • If a defined plan has no prices, don’t return it when fetching available plans
      • update active_features when manually changing active_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 buy plan_c, the new value of active_plans should be plan_a, plan_b, plan_c
        • Now I manually add plan_d to the active_plans field, the new value should be plan_a, plan_b, plan_c, plan_d
        • When plan_a is deactivated, the new value should be plan_b, plan_c, plan_d, even though plan_d was not bought via a subscription.
  • Build migration
    • Maybe migrate over old data from entity_subscription table to the new way of storing / registering subscriptions
    • Remove old tables and columns
      • license stuff
        • license_plan_module table
        • license_plans table
        • licenses table
        • All fields, need to find them
      • old subscription stuff
        • module_subscription table
        • module_subscription_prices table
        • entity_subscription table
        • All fields, need to find them
    • 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 or description 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>
  • entity.hasActiveFeature(<slug>)
    • Api recipes
    • SPA recipes