Plans and pricing
AspFox ships with three plans. The amounts are placeholders — you set your own prices in Stripe and update the Price ID environment variables. No code changes are needed to change pricing.
Plans are configured in Stripe Dashboard. The Price IDs for Pro and Business are stored in
STRIPE_PRO_PRICE_ID and STRIPE_BUSINESS_PRICE_ID in your .env. The Free plan has no Stripe Price ID — it uses local state only.
Checkout flow
tenantId in the Checkout Session metadata is what ties the Stripe event back to the right tenant in AspFox. Without it, webhooks would have no way to update the correct tenant’s subscription.
Webhook processing and idempotency
Stripe can deliver the same webhook more than once. It also retries on any non-2xx response. AspFox handles this with an idempotent processing pattern: every handler checks current state before making changes, so running the same handler twice with the same event produces the same result.AspFox returns
200 OK for all webhook events, including events it does not handle. Stripe requires a 2xx response to consider a webhook delivery successful. Returning non-2xx for unhandled events causes Stripe to retry indefinitely.Subscription status middleware
SubscriptionMiddleware runs on every authenticated request and reads the current tenant’s subscription status. It does not query the database on every request — it reads from a Redis cache with a 5-minute TTL.
Subscription entity fields
Admin manual override
The admin panel has a manual subscription status toggle atPATCH /api/v1/admin/tenants/{tenantId}/subscription. This updates the local subscription record only — it does not touch Stripe.
Use this for support cases where Stripe and local state have diverged (e.g., a webhook was missed, or you manually refunded a charge in Stripe). The SubscriptionSyncJob runs every 2 hours and will re-sync with Stripe, so manual overrides may be reverted unless you also correct the state in Stripe.