Subscription checkout requires subject because recurring access belongs to an app-defined billing owner, such as a user, team, organization, workspace, tenant, or group.
Do not treat the success URL as proof of payment. Use webhook-projected payment state to fulfill orders and grant subscription access.
const { data, error } = await insforge.payments.createCustomerPortalSession({ environment: 'test', subject: { type: 'team', id: teamId }, returnUrl: `${window.location.origin}/billing`});if (error) { if ('statusCode' in error && error.statusCode === 404) { // No Stripe customer mapping exists yet. Show a subscribe CTA instead. return; } throw error;}if (data?.customerPortalSession.url) { window.location.assign(data.customerPortalSession.url);}
Customer portal sessions require an authenticated user and an existing Stripe customer mapping for the subject. The mapping is usually created after a Checkout Session completes and Stripe returns a customer.
The SDK methods call runtime routes using the current InsForge token. The backend inserts local session rows using the caller context:
payments.checkout_sessions for Checkout attempts
payments.customer_portal_sessions for Billing Portal attempts
Before exposing subscription checkout or customer portal UI, add app-specific RLS policies for these tables. For example, if subscriptions belong to teams, only team members should be able to create sessions for that team.
Do not let users submit arbitrary subject.type and subject.id values unless your app has matching RLS policies.
The Payments SDK does not expose generic end-user reads for payments.subscriptions or payments.payment_history. Those tables are admin projections.For user-facing billing state, create app-owned tables with RLS and populate them from payment projection triggers:
public.orders
public.credit_ledger
public.user_entitlements
public.team_billing_status
Populate those tables from webhook-projected payment state. See Payments Architecture for examples.
Do not mark an order paid, grant credits, or activate a subscription from the success URL alone. Stripe recommends webhooks for reliable fulfillment because customers might complete payment without returning to your app.
Then add a database trigger that listens to payments.payment_history and updates public.orders when the matching row becomes type = 'one_time_payment' and status = 'succeeded'. The success page can read the order and show pending, paid, or fulfilled.
Use environment: 'test' while developing. Only switch to live after the developer explicitly approves production Stripe changes and live prices are configured.
Never put Stripe secret keys in frontend code or public deployment environment variables. Configure Stripe keys through the InsForge dashboard or CLI.