Kizaki
Guides

Payments

Use managed payments when you want Kizaki to own checkout, subscription state, and Stripe synchronization instead of rebuilding those paths yourself.

Payments are worth treating as infrastructure, not as a handful of helper functions.

If your app sells plans, trials, or seats, the dangerous part is not deciding which feature belongs to Pro. The dangerous part is everything around it: mapping customers to subscriptions, keeping local state in sync with Stripe, handling failures, and making sure your product can reason about payment state without every workflow re-implementing billing logic.

That is the job of managed payments.

When To Use The Managed Surface

Use the payments block when you want Kizaki to own:

  • plan-to-Stripe price resolution
  • checkout session creation
  • subscription state stored locally by the platform
  • principal.plan and principal.planStatus
  • Stripe webhook verification and deduplication
  • payment lifecycle effects
  • local payment simulation during development

If your billing model is unusual or Stripe-specific enough that the managed surface is too narrow, you can still use raw Stripe directly. The important part is making that a deliberate choice rather than drifting into it accidentally.

For most SaaS-style apps, the clean path is:

  1. declare plans in Inspire
  2. point the schema at a Stripe secret
  3. decide whether any plan is per-seat
  4. define a grace policy
  5. gate product access with principal.plan
  6. use the generated payment API for checkout, portal access, and subscription reads

That gives you one shared payment vocabulary across the schema, runtime, and app code.

Start With A Small Plan Model

payments {
  @secret stripeKey

  plan Free {}

  plan Pro {
    stripe: {
      monthly: "price_pro_monthly",
      annual: "price_pro_annual",
    },
  }

  gracePeriod: 7d,
  graceAction: downgrade,
}

Start with the product plans your app actually needs. You can always grow the payment model later, but vague plans and Stripe IDs scattered through the codebase get hard to reason about quickly.

Keep Feature Gating In Product Terms

The point of the payments model is that the rest of your app should not care about Stripe Price IDs.

Use plan-aware access in product language:

entity AdvancedReport {
  title: string,

  @grant read where principal.plan in [Pro, Team]
}

And use runtime checks in server code when the rule belongs to a workflow rather than raw data access:

const sub = await inspire.payments.getSubscription();

if (sub.plan === "Free") {
  throw new ClientError("Upgrade required to export reports.");
}

That split stays clean:

  • policies protect data
  • application code decides feature behavior
  • the payment system supplies the shared plan state

Use Per-Seat Billing Only When Seats Are Real Rows

Per-seat billing works best when a seat is already a first-class entity in your schema.

payments {
  @secret stripeKey

  plan Team {
    stripe: "price_team_monthly",
    perSeat: true,
  }

  seatEntity: OrgMembership,
  gracePeriod: 7d,
  graceAction: downgrade,
}

That way the billing quantity follows your actual membership model instead of an application-maintained counter that can drift.

If your app does not already have a real entity that represents seats, define that model first. Seat billing should follow the product shape, not invent it.

Model Failure Behavior Up Front

Grace handling is not an edge case. It is part of the product.

Decide early whether failed payments should:

  • downgrade access after the grace window, or
  • move the account into a suspended state that your app treats differently

Being explicit here is important because a missing payment-failure policy creates confusion everywhere else: support behavior, UX messaging, policy gating, and internal reporting.

Use Payment Effects For Reactions, Not State

Payment effects are the right place for downstream reactions:

  • send a receipt
  • notify a team channel
  • start a churn workflow
  • remind a trial user before expiry

They are not the right place to define subscription truth. Subscription truth should remain in the platform-managed payment layer.

payments {
  @secret stripeKey

  plan Free {}
  plan Pro { stripe: "price_pro_monthly" }

  gracePeriod: 7d,
  graceAction: downgrade,

  @on(payment.succeeded) effect sendReceipt {
    retries: 5,
    backoff: exponential(1s),
  }
}

That division of responsibility keeps the system stable:

  • the platform owns the billing state
  • effects own your reaction to that state

The Runtime Workflow

Once payments are configured, the normal runtime path is:

  1. your app asks Kizaki to create checkout
  2. the platform resolves the plan to a Stripe Price
  3. the platform finds or creates the Stripe customer mapping
  4. Stripe completes checkout
  5. the platform handles webhook verification and subscription updates
  6. your app reads the result through principal.plan, principal.planStatus, or getSubscription()

That is what makes managed payments useful. You do not build and re-build the same Stripe glue in every app.

Development Workflow

Most payment work should not require a live Stripe account during normal iteration.

Use the local mock first:

kizaki dev payments set-plan Pro
kizaki dev payments set-status past_due
kizaki dev payments simulate payment.failed

That is enough to validate:

  • plan-aware policies
  • upgrade prompts
  • grace-period UI
  • payment lifecycle effects

Use kizaki dev --stripe-live only when you need to test Stripe’s actual behavior.

When To Reach For Raw Stripe

Use raw Stripe directly when the feature is truly provider-specific or outside the managed surface, for example:

  • refunds
  • promotion-specific workflows
  • custom invoicing
  • advanced portal configuration
  • unusual subscription changes

When you do that, keep one rule in mind: raw Stripe should extend the payment system, not fork it. Use the managed payment state as the app’s source of truth whenever possible.

  • keep plans simple and product-facing
  • do not spread Stripe Price IDs across your codebase
  • gate product access through principal.plan and principal.planStatus
  • use managed checkout and subscription state for the default path
  • use payment effects for notifications and side effects
  • test most payment flows through kizaki dev payments before touching live Stripe

On this page