Documentation Index
Fetch the complete documentation index at: https://docs.aspfox.com/llms.txt
Use this file to discover all available pages before exploring further.
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.| Plan | Monthly price (placeholder) | Free trial |
|---|---|---|
| Free | $0 | None |
| Pro | $29/mo | 14 days |
| Business | $79/mo | 14 days |
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.| Event | What AspFox does |
|---|---|
checkout.session.completed | Creates local Subscription record; sets status to Trialing or Active depending on whether a trial was started. Idempotency: checks if subscription already exists before creating. |
customer.subscription.updated | Updates local subscription status, current period end, plan, and cancel_at_period_end flag. Idempotency: compares incoming values to current values before writing. |
customer.subscription.deleted | Sets local status to Canceled. Idempotency: if already Canceled, no-op. |
invoice.payment_failed | Sets local status to PastDue. Sends payment failed email. Creates SubscriptionPastDue notification. |
invoice.payment_succeeded | If status was PastDue, sets to Active. Sends payment recovered email. Creates PaymentRecovered notification. If already Active, no-op. |
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
| Field | Type | Description |
|---|---|---|
TenantId | Guid | Which tenant this subscription belongs to |
StripeCustomerId | string | Stripe Customer ID (cus_…) |
StripeSubscriptionId | string? | Stripe Subscription ID (sub_…); null on Free plan |
Plan | enum | Free, Pro, or Business |
Status | enum | Active, Trialing, PastDue, Canceled, Paused |
CurrentPeriodEnd | DateTime? | When the current billing period ends |
CancelAtPeriodEnd | bool | Whether the subscription will cancel at period end |
TrialEndsAt | DateTime? | When the trial expires; null if no trial |
UpdatedAt | DateTime | When this record was last modified by a webhook |
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.