Stripe Billing on the Stripe Sync Engine, customer-invoice payment surfaces, environment variables, Connect setup, webhooks, and go-live checks.
CentraKit uses Stripe for two distinct money flows:
stripe schema of the database, and the better-supabase billing block links each organization to its Stripe customer (better_supabase.billing_customers). CentraKit keeps only its plan catalog; usage is counted by the better-supabase usage block.Never collect or store raw card numbers, CVC, bank account details, legacy Tokens, Sources, Charges, or Card Element data. Prefer the Stripe Payment Element. Never persist PaymentIntent client secrets.
subscription mode start new subscriptions and upgrades. The billing block creates the Stripe customer on the first checkout, with the organization id in metadata.organization_id, and links it.organization_invoice_recipients.public.organization_subscription(organization_id) matches the subscription item's price to the plan catalog (billing_plan_prices.stripe_price_id, or the price metadata plan_key, credit_package_key and included_credits), and billing.organization_plans gives the plan every organization is on (Starter without an active, trialing or past due subscription). Checkout and Portal return pages are UX hints only.organization_subscription_history stays CentraKit's analytics record: after each customer.subscription.* event and each refresh, capture_organization_subscription_history appends a row when the plan, status, quantity or schedule changed.invoice_pdf or Hosted Invoice Page URLs.usage_stripe_report job, as meter events named by billing_usage_meters.stripe_event_name. Only the overage above each plan's included quantity in the billing period is sent; a meter that is unlimited on the plan sends nothing, and organizations without a Stripe customer are skipped until they have one.entitlements module (plans source) reads billing.organization_plans and the included rows of billing_plan_features. A feature row can carry a value, which better_supabase.tenant_entitlement_value returns. Audit-log retention is the value of the audit-logs feature in days (Starter 1, Pro 7, Business 90, Enterprise 365, or the audit_log_retention_days metadata on the Enterprise Stripe subscription), and file history is the value of file-history./{orgSlug}/payments (payment.manage).charges_enabled + enabled status).transfer_data.destination and application_fee_amount from the admin platform_settings.invoice_payment_fee row (percentage bps or fixed minor units; default 150 bps = 1.5%)./{orgSlug}/portal/invoices/[invoiceId] via the embedded Payment Element. Dynamic payment methods (card, iDEAL, etc.) follow Stripe Dashboard + currency/country configuration.invoice_payments. Webhooks update payment + invoice status atomically; browser returns only refresh UI.invoice_payments status.Set these per environment in Vercel (Stripe integration covers the keys):
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
POSTGRES_URL=postgres://...POSTGRES_URL (the Supabase integration sets it, as does POSTGRES_URL_NON_POOLING) is the direct database connection the billing block and the Stripe Sync Engine use, in both the SaaS app and the API app.
Plan price IDs are not environment variables. Keep them in Stripe and the platform database. Use separate test and live products, prices, Portal configuration, Connect settings, and webhook signing secrets. Enable Connect in the Stripe Dashboard and complete platform profile / approval prerequisites before going live with destination charges.
CentraKit runs the Sync Engine as the @supabase/stripe-sync-engine library inside the API app rather than as a separate service. The Stripe webhook route verifies each event and stores it in the webhook inbox. The webhook_inbox background job then hands every platform event to the billing consumer processStripeBillingEvent (packages/core-platform/src/billing-webhooks.ts), which lets the Sync Engine store the object (StripeSync.processEvent) and then lets the billing block link the customer and emit its billing.* events. Customer-invoice payment events go to the invoice payment handler instead. Events the engine does not store (Connect account.updated, for example) skip it.
The engine owns the stripe schema. CentraKit's declarative schema and pnpm supabase:diff never touch it; pnpm stripe:sync:migrate installs and upgrades it with the engine's own migrations (tracked in stripe.migrations). The script first creates stripe.invoice_status, because engine 0.48.5 checks for an existing invoice_status type in every schema and would otherwise skip it next to CentraKit's public.invoice_status. Stripe timestamps are integer columns in the engine's schema, so dates after January 2038 cannot be stored.
Do these once per Supabase project (production main and the persistent develop branch), and again after a Sync Engine upgrade:
pnpm stripe:sync:migrate --db-url "<POSTGRES_URL_NON_POOLING>". It is idempotent. Do not install the dashboard's Stripe Sync Engine integration on top of it, because both would manage the same schema.STRIPE_SECRET_KEY=<key> pnpm stripe:sync:backfill --db-url "<POSTGRES_URL_NON_POOLING>" (--object subscription or --since 2026-01-01 narrow it).POSTGRES_URL is set for the SaaS and API projects in all three Vercel environments./api/v1/billing/stripe/webhook with the events listed below, and set STRIPE_WEBHOOK_SECRET to its signing secret./admin/plans, so billing_plan_prices.stripe_price_id maps each Stripe price to its plan; prices created in the Stripe Dashboard need plan_key, credit_package_key and included_credits metadata instead.audit_log_retention_days in the Stripe subscription's metadata.When the tables drift (a missed webhook), Refresh from Stripe in the admin billing detail fetches the organization's subscriptions, invoices, payment methods and tax ids again (syncFromStripe).
pnpm supabase:reset and pnpm supabase:sync run pnpm stripe:sync:migrate --local --demo, which installs the Sync Engine schema in the local stack and loads the demo rows from packages/stripe/fixtures/sync-engine-demo.sql for the seeded organizations. Stripe keys are placeholders locally, so the billing pages read the demo rows and every call to Stripe (checkout, portal, plan changes) fails with a provider error. With real test keys:
/admin/plans.centrakit_ai_input_tokens, centrakit_ai_output_tokens, centrakit_inbox_messages_sent, centrakit_api_requests, and centrakit_mcp_tool_calls.pnpm env:pull) and POSTGRES_URL points at the local database in .env.development.local.payment_intent.*, charge.refunded, charge.dispute.created, and account.updated for invoice payments)./{orgSlug}/billing and /{orgSlug}/usage with billing.manage, and /{orgSlug}/payments with payment.manage.The webhook endpoint verifies the stripe-signature header, stores each Stripe event once by its event id in the webhook inbox and answers 202. A repeated delivery of a stored event gets 200 and is not processed again. The inbox worker processes stored events within a minute, retries a failed event with backoff, and gives it up after eight attempts; the admin dashboard lists failed and given-up events.
The Sync Engine stores these; the billing block also handles checkout.session.completed and customer.subscription.*.
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.createdcustomer.updatedcustomer.tax_id.createdcustomer.tax_id.updatedcustomer.tax_id.deletedprice.createdprice.updatedpayment_method.attachedpayment_method.detachedinvoice.createdinvoice.finalizedinvoice.finalization_failedinvoice.paidinvoice.payment_failedinvoice.payment_action_requiredinvoice.upcominginvoice.voidedinvoice.marked_uncollectibleRouted by PaymentIntent/charge metadata (centrakit_purpose=customer_invoice_payment) or Connect account.updated:
payment_intent.succeededpayment_intent.payment_failedpayment_intent.canceledpayment_intent.requires_actioncharge.refundedcharge.dispute.createdaccount.updatedbilling_usage_meters.stripe_event_name.STRIPE_WEBHOOK_SECRET is the live signing secret.pnpm stripe:sync:migrate and pnpm stripe:sync:backfill against the production database (see Hosted setup)./admin/settings invoice payment fee matches the commercial agreement.Last updated on