Operations & platform
Subscriptions
Indicative price, not an offer: €39/mo; 2% per recurring transaction or €199/mo flat; support for dunning, tax recalc on each cycle
Generated from docs/plan/paid/subscriptions/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
Merchants need recurring revenue through subscriptions: automated billing cycles, retry logic for failed cards, pause/resume/cancel workflows, and tax recalculation on each renewal. This feature provides a complete subscription engine with dunning (retry on failure) and lifecycle management.
The problem
I want to sell subscriptions for recurring revenue, but manually billing customers every month is unreliable and doesn’t scale. I need failed charges to retry automatically, tax to recalculate, and customers to be able to pause or cancel.
What it does
- Subscription product type: recurring interval (daily/weekly/monthly/annual) with a fixed price in the shop’s currency
- Subscription order lifecycle: created → active → paused/cancelled → expired/ended
- Billing cycle: every N days/weeks/months, calculate due date, create a renewal invoice, attempt payment
- Retry policy (dunning): on payment failure, retry on day 3, day 6, day 9; cancel on 3rd failure
- Proration: if customer upgrades mid-cycle, calculate credit for remaining days and adjust next billing date
- Tax recalculation: on each renewal, recompute sales tax based on current address (may have changed)
- Customer portal: view active/paused subscriptions, next billing date, payment method, update payment, pause/resume/cancel
- Admin dashboard: subscription status, active/paused/cancelled counts, MRR (monthly recurring revenue), churn rate
- Webhook events: subscription.created, subscription.renewed, subscription.paused, subscription.resumed, subscription.cancelled, subscription.payment_failed, subscription.payment_succeeded
- API endpoints: CRUD subscriptions, trigger renewal, handle customer-initiated changes
What it deliberately does NOT do
Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.
- Multi-currency subscriptions — subscriptions price in the shop’s single currency; splitting across currencies is a separate, larger feature
- Proration rules beyond simple day-based credit — complex accounting (e.g., staggered billing across multiple products) is out of scope
- Marketplace subscriptions (each subscription can have only one merchant) — multi-party revenue splits are guest-author-payouts, not subscriptions
- Usage-based/metered billing — charge per API call or data tier; this is a different product entirely
- Subscription gift cards — a gift subscription is a separate sales flow; treating subscriptions as giftable is post-1.0
Data model
New entity: Subscription {id, customer_id, product_id, interval, interval_count, price_minor_units, currency, status, started_at, next_billing_date, paused_at, cancelled_at, ended_at, failure_count, last_payment_attempt, created_at, updated_at}. New entity: SubscriptionBillingCycle {id, subscription_id, due_date, invoice_id, status, attempt_count, last_attempted_at, completed_at}. Schema migration for all three drivers. Reference existing customer, product, and order entities.
API
- POST /api/subscriptions — create subscription (authenticated customer or admin)
- GET /api/subscriptions/:id — fetch subscription details
- GET /api/customers/:customer_id/subscriptions — list active/paused/cancelled
- PATCH /api/subscriptions/:id — update subscription (payment method, pause/resume)
- DELETE /api/subscriptions/:id — cancel subscription
- POST /api/subscriptions/:id/billing-cycles — trigger renewal manually (admin only)
- GET /api/subscriptions/:id/billing-cycles — list billing history
- GET /api/admin/subscriptions/metrics — MRR, churn, active count (admin only)
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Subscription manager: dashboard with MRR card, active/paused/cancelled counts, trend chart. List view with filter by status/product, inline actions (view, pause, resume, cancel, manually trigger billing). Subscription detail panel: customer info, product, interval, next billing date, retry history, payment method. Bulk actions: pause/cancel/trigger billing for multiple subscriptions. Alerts: upcoming renewals, retry failures, at-risk churn.
The seam — why this is paid
Core owns the subscription entity model, billing cycle tracking, retry state machine, and REST API. Paid layer owns payment processor integration (tokenizing recurring payment methods, executing recurring charges, handling disputes), tax recalculation (looking up sales tax rules by jurisdiction on each cycle), and PCI compliance (recurring payment tokens never touch core; they live in the payment processor).
Support commitment: PCI compliance on recurring billing, subscription math (proration, pause)
Dependencies
- Customer and product entities (already shipped)
- Order and invoice system (already shipped)
- Payment processor with recurring billing API (Stripe, Adyen, or equivalent) must exist
- Scheduler (already shipping in core) to trigger nightly billing cycle evaluation
- Assume: merchant has a single primary currency
Acceptance checks
Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.
- A monthly subscription product can be created with a price and interval; it is stored and queryable
- An active subscription generates a billing cycle on its next_billing_date; the cycle has status ‘pending’
- When a billing cycle is attempted and the payment succeeds, it creates an order/invoice and sets next_billing_date to 30 days later
- When a payment fails, the failure_count increments and a retry is scheduled for day 3; retries stop after 3 failures and subscription is cancelled
- A customer can pause a subscription; it remains stored but no billing cycles are generated until resumed
- A cancelled subscription cannot be resumed; it is archived and excluded from ‘active’ queries
- If a subscription price is €10/month and a customer upgrades after 10 days to €15/month, the next charge is €10 * (20/30) = €6.67 (rounded to minor units, then added to €15)
- On renewal, if customer’s tax address has changed, sales tax is recalculated and applied to the new billing cycle
Risks
Payment retry logic must be idempotent — retrying a cycle that already paid must not double-charge. Proration is arithmetic-heavy and error-prone; floating-point math on money is forbidden, so all calculations must be done in integer minor units with explicit rounding rules. Concurrent payment attempts could race; subscriptions need row-level locking during billing evaluation. If payment processor is down, billing cycles must queue and retry when service restores. Tax lookups may fail; decide whether to bill without tax or defer the charge until tax can be calculated. Refunding a subscription payment after the customer has cancelled may trigger chargeback disputes.
Commercial context
| Suggested price | €39/mo; 2% per recurring transaction or €199/mo flat; support for dunning, tax recalc on each cycle |
| Rival anchor | Shopify Subscriptions: free (core) + 2% per transaction; Recharge: $99/mo+ |
The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.
Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full
catalogue and ../../AI-GUIDE.md for how to work on this repository.