The promotions module stores discount and custom-price rules in the database. Base prices remain in the product-specific pricing source of truth: marketing prices come from src/config/marketing-pricing.ts, while app prices come from src/lib/payments/plans.ts.
Promotion management is available to administrators at /admin/promotions. Customer-facing pages only resolve a code and display or apply the resulting price.
Marketing URL
Pass the promotion code in the promo query parameter:
https://your-domain.example/?promo=LAUNCH20
The landing page reads promo, previews the code for each marketing product, displays the discounted price, and adds the same parameter to the checkout link. The checkout endpoint therefore receives URLs such as:
/api/checkout?product=full&promo=LAUNCH20
The parameter is case-insensitive and is normalized server-side. App billing accepts the same promotion concept through its billing form rather than through a page URL.
Product keys
Promotion rules use a namespaced product key so products with the same ID do not collide:
| Product key | Applies to |
|---|---|
app:pro | The app's pro plan |
template:full | The marketing template's full tier |
app:* | Every product in the app namespace |
template:* | Every product in the template namespace |
plugin:* | Every product in a custom namespace |
An exact key takes precedence over a namespace wildcard when both rules exist. The namespace is not limited to app and template; new product namespaces can be added without a schema change.
Pricing modes
percentstores a percentage from 0 to 100.fixed_discountstores a discount in the currency's minor units.fixed_pricestores the resulting price in minor units.
The checkout flow calculates the final amount server-side from the canonical base price. It passes the custom amount to the active payment adapter and disables provider-side promotion entry for that checkout.
Feature flag
ENABLE_PROMOTIONS="false"
When disabled, the admin page and navigation item are hidden, /admin/promotions returns 404, and checkout and marketing pages ignore promotion codes.