Payments
Declare plans, Stripe price mappings, grace rules, and payment effects in Inspire.
The payments block declares your billing model: plans, Stripe price mappings, trial windows, grace rules, seat-tracking, and payment-triggered effects. The platform owns checkout, customer mapping, subscription state, webhook verification, and idempotency. Your app code decides what each plan unlocks.
Syntax
payments {
@secret stripeKey
plan Free {}
plan Pro {
stripe: {
monthly: "price_pro_monthly",
annual: "price_pro_annual",
},
}
plan Team {
stripe: "price_team_monthly",
perSeat: true,
legacyPrices: ["price_team_v0"],
}
seatEntity: OrgMembership,
billingEntity: Organization,
trial: [Pro: 14d, Team: 7d],
gracePeriod: 7d,
graceAction: downgrade,
@on(payment.succeeded) effect sendReceipt {
retries: 5,
backoff: exponential(1s),
timeout: 30s,
}
@on(subscription.canceled) effect handleChurn {
retries: 3,
backoff: exponential(2s),
}
}What The Platform Owns
- Mapping users to Stripe customers
- Looking up the correct price for a plan
- Tracking subscription state locally
- Feeding
principal.planandprincipal.planStatus - Handling Stripe webhooks safely
- Deduplicating repeated webhook deliveries
- Exposing a narrow server-side payment API
What you decide: whether Pro unlocks exports, whether Team raises limits, whether a feature stays available during past_due.
Plans
Declare plans in product terms, not provider IDs.
payments {
@secret stripeKey
plan Free {}
plan Pro {
stripe: {
monthly: "price_pro_monthly",
annual: "price_pro_annual",
},
}
}The first plan without a Stripe price is the default free plan, used when no active subscription exists.
Single-Price And Multi-Interval Plans
Map a plan to either a single Stripe Price ID (stripe: "price_id") or a monthly/annual pair (stripe: { monthly: "...", annual: "..." }).
Legacy Prices
Use legacyPrices when existing subscribers are attached to older Stripe Prices that should resolve to the same plan.
plan Pro {
stripe: {
monthly: "price_pro_monthly",
annual: "price_pro_annual",
},
legacyPrices: ["price_old_pro_v1", "price_old_pro_v2"],
}Inbound mapping for webhook and refresh resolution. Change Stripe pricing over time without losing the ability to interpret existing subscriptions.
Secrets
payments {
@secret stripeKey
// ...
}Environments supply the real value through kizaki secrets. The schema declares which secret is required.
Per-Seat Billing
Set perSeat: true on plans whose quantity follows a row-count.
plan Team {
stripe: "price_team_monthly",
perSeat: true,
}
seatEntity: OrgMembership,seatEntity declares which entity represents a seat (typically a membership table). The compiler enforces that perSeat: true requires a seatEntity.
Billing Entity
billingEntity: Organization,Declares which entity is the billable account. Use when billing belongs to an organization or tenant rather than an individual. Omit for per-user billing.
Trials
trial: [Pro: 14d, Team: 7d],Declares trial eligibility and duration per plan.
Grace Periods
gracePeriod: 7d,
graceAction: downgrade,gracePeriod sets how long a subscription remains in a paid-but-failed state. graceAction determines what happens when the window expires:
| Action | Behavior |
|---|---|
downgrade | Moves to the default free plan |
suspend | Marks the subscription as suspended |
The compiler requires a grace policy when paid plans are declared.
Payment State In The Rest Of The App
The runtime resolves principal.plan and principal.planStatus. Policies and application code reference plans directly.
In Policies
entity AnalyticsReport {
title: string,
@grant read where principal.plan in [Pro, Team]
}Combine plan and status for stricter access:
entity LiveDashboard {
title: string,
@grant read
where principal.plan in [Pro, Team]
and principal.planStatus in [active, trialing]
}In Server Code
The managed payment surface includes:
- Checkout creation
- Subscription lookup
- Billing portal creation
- Payment-state refresh from Stripe
- Stripe customer ID lookup for raw Stripe interop
Payment Effects
Durable effects tied to payment lifecycle events instead of row mutations.
@on(payment.failed) effect notifyPaymentFailure {
retries: 3,
backoff: exponential(1s),
}Available events:
| Event |
|---|
payment.succeeded |
payment.failed |
subscription.created |
subscription.changed |
subscription.canceled |
subscription.trialEnding |
subscription.pastDue |
Use for receipts, trial-expiry emails, churn handling, and internal notifications.
Managed Payments Vs Raw Stripe
Managed payments are opt-in. If your billing model fits the managed surface, use it. If you need a Stripe feature outside that surface, use Stripe directly. The runtime supports a refresh bridge from Stripe back into managed payment state.
Compiler Validations
The compiler checks:
- Paid plans without a grace policy
perSeat: truewithout aseatEntity- Unknown plan names in policy conditions
- Unknown payment event names on effects
- Overlapping
legacyPricesand current Stripe prices
Dev And Operations
| Command | Purpose |
|---|---|
kizaki dev --stripe-live | Use Stripe test mode instead of local mock |
kizaki dev payments set-plan | Set plan without Stripe |
kizaki dev payments set-status | Set subscription status without Stripe |
kizaki dev payments simulate | Simulate billing flows |
kizaki payments refresh | Reconcile from Stripe |
kizaki payments refresh --all | Reconcile all subscriptions |
Most development should use the mock. Real Stripe is only necessary when validating provider behavior.
Recommended Usage
- Declare a small set of product-facing plans
- Keep Stripe identifiers inside the payments block
- Gate features with
principal.planandprincipal.planStatus - Use
legacyPriceswhen migrating pricing - Model seat billing from a real membership entity
- Use payment effects for lifecycle reactions, not primary state management
Related guide: Payments