Stripe Integration
Overview
MysticX uses Stripe for subscription billing and one-time credit pack purchases, integrated through the Better Auth Stripe plugin.
Products
Web Subscriptions
| Product | Monthly | Yearly | Yearly Credit Grant |
|---|---|---|---|
| Gold | $19.99/mo (6,000 SE/mo) | $89.99/yr (62% off) | 100,000 SE one-time |
| Diamond | $69.99/mo (30,000 SE/mo) | $299.99/yr (64% off) | 1,000,000 SE one-time |
Yearly plans deposit credits as a one-time lump sum when the subscription is created or renewed. Legacy yearly subscribers on the old pricing ($167.92/$587.92) continue to receive monthly-style grants until they migrate to the new yearly price IDs.
Credit Packs (One-Time — Web Only)
| Pack | Credits | Price |
|---|---|---|
| Taster | 250 | $1.99 |
| Mini | 600 | $3.99 |
| Starter | 2,000 | $9.99 |
| Best Value | 4,000 | $14.99 |
Gold subscribers receive +10% bonus credits; Diamond subscribers +15% — at the same price.
Credit packs use ad-hoc price_data (no pre-created Stripe Price IDs needed).
Setup
1. Stripe Dashboard Configuration
Products and Prices
Create products in Stripe Dashboard:
- Gold Monthly — $19.99/mo recurring
- Gold Yearly — $89.99/yr recurring (new —
STRIPE_PRICE_GOLD_YEARLY_V2) - Diamond Monthly — $69.99/mo recurring
- Diamond Yearly — $299.99/yr recurring (new —
STRIPE_PRICE_DIAMOND_YEARLY_V2)
Set the Price IDs in environment variables. The old yearly Price IDs (STRIPE_PRICE_GOLD_YEARLY / STRIPE_PRICE_DIAMOND_YEARLY) remain configured for backward compatibility with legacy subscribers.
Webhook Endpoint
Point the webhook to: {APP_URL}/api/auth/stripe/webhook
Required events:
checkout.session.completedinvoice.payment_succeededcustomer.subscription.updatedcustomer.subscription.deleted
Customer Portal
Enable the Stripe Customer Portal with these settings:
- Proration: immediately for upgrades
- Downgrades: at period end
- Cancellations: at period end
- Allow customers to update payment methods
2. Environment Variables
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...Subscription Lifecycle
New Subscription
- User clicks "Subscribe" on pricing page
- App creates a Stripe Checkout Session via Better Auth
- Stripe redirects to checkout
- On completion, webhook fires
checkout.session.completed - App updates user tier and creates subscription record
- Credits are granted (monthly grant + any proration)
Upgrade (Gold → Diamond)
- Stripe processes mid-cycle proration (immediate charge for difference)
- Webhook fires
customer.subscription.updated - App updates tier to DIAMOND
- Credit differential is granted
Downgrade (Diamond → Gold)
- Change takes effect at period end (no immediate charge)
- Webhook fires at next billing cycle
- App updates tier to GOLD
Cancellation
- User cancels via Customer Portal or app
- Subscription status changes to
cancel_at_period_end - Access continues until period end
- Webhook fires
customer.subscription.deletedat period end - App updates tier to FREE
Failed Payment
- Stripe retries per dunning schedule (recommended: 3 retries over 2 weeks)
- If all retries fail, subscription is canceled
- App handles graceful degradation
One-Time Credit Purchases
- User selects a credit pack in the CreditPurchaseModal
- App creates a Stripe Checkout Session with ad-hoc
price_data - On completion, webhook grants credits with tier bonus
- Transaction is recorded in
CreditTransaction(idempotent by invoice ID)
Security
- Rate limiting: 5 purchase attempts per 60 seconds per user
- Idempotency: All credit grants check existing
CreditTransactionby invoice ID - Metadata integrity: Checkout sessions include user ID and pack metadata for verification
- Webhook signature verification: All webhooks validated against
STRIPE_WEBHOOK_SECRET
Polling on Return
After Stripe redirects back to the app:
- Client polls subscription/credit status (up to 5 attempts, 2-second intervals)
- Handles race conditions where webhook arrives after redirect
- Shows success state once confirmed
Key Implementation Notes
- Stripe Customer ID is created automatically on first purchases via Better Auth plugin
- Customer ID is persisted on the user record (
stripeCustomerId) - All subscription state changes flow through webhooks (never direct API polling for state)
- The membership page at
/membershipshows current plan, upgrade/downgrade options, and manage subscription button