Skip to main content

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

The 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 at PATCH /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.