Plans & Stripe checkout
Chapter 14 covered PII and legal text. This chapter wires paid plans the way a real SaaS does: tier caps in the app, Stripe for checkout, and a sysadmin surface to flip billing live.
User story: Mira outgrows Free
Section titled “User story: Mira outgrows Free”Mira’s on Free (1 event, 50 RSVPs). The rooftop launch fills the list; she creates a second event and hits the cap. The host UI shows usage on Plan & billing and offers Starter or Pro — same numbers as the pricing page from chapter 12.
Locally you can mount Stripe test keys and complete checkout. Prod demo
(show-pony.kumiko.rocks) stays read-only for writes — you still see tier,
usage, and the billing screen; checkout needs a local install or
billingLive=false until a sysadmin enables it.
On the cloud demo the host tenant stays on Free (one event cap) — the boot
seed creates Rooftop Launch on demo and Acme Offsite on the separate
acme tenant (one event each). Grant a paid tier via Platform → tier admin +
DB reset if you need multiple events on a single tenant (chapter 11).
| Surface | Local | Cloud |
|---|---|---|
| Host login (apex) | http://show-pony.localhost:4180/login | https://show-pony.kumiko.rocks/login |
| Demo invite (rooftop) | http://demo.show-pony.localhost:4180/e/rooftop-launch | https://demo.show-pony.kumiko.rocks/e/rooftop-launch |
| Acme invite (offsite) | http://acme.show-pony.localhost:4180/e/acme-offsite | https://acme.show-pony.kumiko.rocks/e/acme-offsite |
| Marketing (EN) | http://show-pony.localhost:4180/ | https://show-pony.kumiko.rocks/ |
| Marketing (DE) | http://show-pony.localhost:4180/de | https://show-pony.kumiko.rocks/de |
What gets mounted
Section titled “What gets mounted”| Piece | Role |
|---|---|
tier-engine + SHOWPONY_TIER_MAP | free / starter / pro / studio caps (events + RSVPs) |
cap-counter + cap-guard.ts | Block the (N+1)th event or RSVP with upgrade_required |
billing-foundation + subscription-stripe | Checkout session + webhook → read_subscriptions |
| Webhook route | Sync subscription tier into read_tier_assignments |
| Host Plan & billing screen | Usage, plan cards, Stripe redirect |
| Platform workspace | Tier admin + Stripe settings (config:nav:audience-system) |
Tier numbers live in one place — marketing preview and runtime caps both read
src/marketing/pricing.ts / tier-map.ts.
Run-config (excerpt)
Section titled “Run-config (excerpt)”// Single source of truth for show-pony feature composition.// Both bin/server.ts (dev) and the kumiko-schema CLI build on this,// so the runtime registry and generated schema never drift apart.//// HAS_AUTH=true → composeFeatures automatically pulls in the bundled auth chain// (config/user/tenant/auth-email-password/secrets).
import { createAdminShellFeature } from "@cosmicdrift/kumiko-bundled-features/admin-shell";import { createAuditFeature } from "@cosmicdrift/kumiko-bundled-features/audit";import { billingFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";import { capCounterFeature } from "@cosmicdrift/kumiko-bundled-features/cap-counter";import { createComplianceProfilesFeature } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles";import { createJobsFeature } from "@cosmicdrift/kumiko-bundled-features/jobs";import { mailFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/mail-foundation";import { mailTransportInMemoryFeature } from "@cosmicdrift/kumiko-bundled-features/mail-transport-inmemory";import { createManagedPagesFeature } from "@cosmicdrift/kumiko-bundled-features/managed-pages";import { createRateLimitingFeature } from "@cosmicdrift/kumiko-bundled-features/rate-limiting";import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";import { createTenantLifecycleFeature } from "@cosmicdrift/kumiko-bundled-features/tenant-lifecycle";import { createTierEngineFeature } from "@cosmicdrift/kumiko-bundled-features/tier-engine";import { composePagesStack } from "@cosmicdrift/kumiko-dev-server/compose-stacks";import { appShellFeature } from "./features/app-shell/feature";import { showPonyFeature } from "./features/show-pony/feature";import { DEFAULT_TIER, SHOWPONY_TIER_MAP } from "./features/show-pony/tier-map";import { renderLegalLayout } from "./legal-layout";import { resolveSubdomainPageTenant } from "./tenant-routing";
/** Overview screens + nav only — app-shell owns workspaces host/platform. */const adminShellFeature = createAdminShellFeature({ registerWorkspaces: false, includeTierAdmin: true,});
export const APP_FEATURES = [ ...composePagesStack({ wrapLayout: renderLegalLayout }), createManagedPagesFeature({ resolveApexTenant: resolveSubdomainPageTenant, allowCustomCss: false, }), mailFoundationFeature, mailTransportInMemoryFeature, createRateLimitingFeature(), createAuditFeature(), createJobsFeature(), createTierEngineFeature({ defaultTier: DEFAULT_TIER, tierMap: SHOWPONY_TIER_MAP }), createComplianceProfilesFeature(), createTenantLifecycleFeature(), billingFoundationFeature, createSecretsFeature(), capCounterFeature, adminShellFeature, appShellFeature, showPonyFeature,] as const;
export const HAS_AUTH = true;subscription-stripe mounts in bin/main.ts / bin/server.ts only when
STRIPE_PRICE_STARTER and STRIPE_PRICE_PRO are set (see bin/stripe-billing-env.ts).
API key + webhook secret can come from env (Pulumi) or sysadmin Stripe settings;
billing live is a runtime toggle — keep it off on the public demo until you mean it.
Host billing screen
Section titled “Host billing screen”Custom React screen (Stripe redirect flow) — pattern copied from PublicStatus / Money-Horse:
// showpony:query:billing-info — tier + subscription + whether checkout is enabled// showpony:query:usage — event/RSVP counts vs caps// billing-foundation:write:create-checkout-session → redirect to StripeNav: Account → Plan & billing in the Events workspace.
Cap enforcement
Section titled “Cap enforcement”Event create wraps withStockCap on event:create. Anonymous rsvp:submit
checks guest cap before insert — guests see a friendly “list full” message, not
an upgrade CTA.
Infra (optional)
Section titled “Infra (optional)”When Pulumi stack config includes showPonyStripePriceStarter + showPonyStripePricePro,
createKumikoApp injects STRIPE_* env vars. Webhook URL:
https://show-pony.kumiko.rocks/webhooks/subscription/stripe
Register that endpoint in the Stripe dashboard (test mode for the learning sample).
Try it locally
Section titled “Try it locally”export STRIPE_API_KEY=sk_test_…export STRIPE_WEBHOOK_SECRET=whsec_…export STRIPE_PRICE_STARTER=price_…export STRIPE_PRICE_PRO=price_…bun devSign in as host → Plan & billing. As sysadmin → Platform → Stripe settings → set keys if not in env → toggle billing live → retry upgrade from the host workspace.
Back to the tutorial index: Show Pony.