Plan add-ons

Optional extras customers stack on top of their base plan — extra storage, premium template packs, dedicated sending IP, priority support, AI usage boosts. Each add-on has its own pricing (one-time or recurring) and a free-form JSON payload that the consuming feature reads at runtime.

Create an add-on

  1. Admin → Add-ons → New add-on.
  2. Identity — name (customer-facing), slug (machine readable), description, category (storage / templates / sending / support / other), icon (lucide name).
  3. Pricing — amount in cents, currency, billing interval (one-time / monthly / yearly), trial days for recurring.
  4. Feature payload — JSON dictionary. Any key prefixed bonus_<quota_key> is added on top of the buyer's plan quota by Customer::quota(); boolean bonuses OR onto the plan flag. Examples that work end-to-end today:
    { "bonus_max_subscribers": 5000 }
    { "bonus_max_sends_per_month": 50000, "bonus_max_sends_per_day": 5000 }
    { "bonus_max_ai_calls_per_month": 1000 }
    { "bonus_max_lead_enrichments_per_month": 500 }
    { "bonus_allow_custom_tracking_domain": true }
    Non-bonus keys (like template_pack or support_tier) are stored for reference; the specific consuming feature reads them.
  5. Visibility — Active (shown to customers) + Featured (accent ring on the customer page) + sort order.
Admin add-ons grid · documentation/screenshots/admin-addons.png

/admin/addons — cards with price, category, active subs counter.

Suggested catalogue

Add-onCategoryPricingPayload
Extra 50 GB storagestorage$5/mo{ "extra_storage_gb": 50 }
Premium templates packtemplates$29 one-time{ "template_pack": "premium-2026" }
Dedicated sending IPsending$80/mo{ "dedicated_ip": true }
Priority supportsupport$50/mo{ "priority_tier": "p1" }
AI tokens — 1M extraother$10/mo{ "ai_extra_tokens_per_month": 1000000 }

Customer marketplace (new in v1.2.1)

Buyers see every active add-on at /addons on their sidebar (Account group). Each card shows the price, the bonus summary rendered from the payload (+5,000 subscribers, +50,000 sends/month), and a Get add-on button. Featured add-ons carry an accent ring.

Purchase flow:

  1. Customer clicks Get add-on. A Payment row is created with metadata.kind = addon_purchase. On manual-payment installs, the customer sees "Request sent to admin — you'll be contacted for payment."
  2. Admin approves the payment in Admin → Manual payments (existing surface). The approve action detects kind = addon_purchase and auto-creates a CustomerAddon(status=active) with starts_at = now() and ends_at derived from the add-on's billing interval (month → +30d, year → +365d, one-time → null).
  3. On the customer's next request, Customer::quota() folds the add-on's payload into the effective quota. Numeric bonus_* keys stack; boolean flags OR onto the plan flag.
  4. Customer can cancel any active add-on with the Cancel button on their /addons page. Cancelling flips status to cancelled and stamps ends_at = now(); the bonus applies until the current period ends per billing-interval semantics.

How customer-side enforcement works

Each consuming feature checks the customer's active add-on payload before allowing the action. Pseudocode:

$active = CustomerAddon::query()
    ->where('customer_id', $customer->id)
    ->where('status', 'active')
    ->with('addon')
    ->get();

$bonusStorage = $active->sum(fn ($a) => $a->addon->payload['extra_storage_gb'] ?? 0);
$storageLimit = ($customer->quota('max_storage_mb') ?? 0) + ($bonusStorage * 1024);

Architecture

Two tables:

Provider-agnostic schema — same architecture supports future GitHub Marketplace / Apple In-App Purchase / etc. without table changes.

Customer purchase flow status Add-on catalogue is fully wired admin-side. Customer browse + Stripe-checkout integration ships in v1.1 — for now, admins create customer_addons rows manually (via tinker or future admin "Grant add-on" button) to test the enforcement path.