Kizaki
ReferenceInspire

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.plan and principal.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:

ActionBehavior
downgradeMoves to the default free plan
suspendMarks 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: true without a seatEntity
  • Unknown plan names in policy conditions
  • Unknown payment event names on effects
  • Overlapping legacyPrices and current Stripe prices

Dev And Operations

CommandPurpose
kizaki dev --stripe-liveUse Stripe test mode instead of local mock
kizaki dev payments set-planSet plan without Stripe
kizaki dev payments set-statusSet subscription status without Stripe
kizaki dev payments simulateSimulate billing flows
kizaki payments refreshReconcile from Stripe
kizaki payments refresh --allReconcile all subscriptions

Most development should use the mock. Real Stripe is only necessary when validating provider behavior.

  • Declare a small set of product-facing plans
  • Keep Stripe identifiers inside the payments block
  • Gate features with principal.plan and principal.planStatus
  • Use legacyPrices when migrating pricing
  • Model seat billing from a real membership entity
  • Use payment effects for lifecycle reactions, not primary state management

Related guide: Payments

On this page