Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.insforge.dev/llms.txt

Use this file to discover all available pages before exploring further.

Payments is a private preview feature. APIs and behavior may change.

Overview

InsForge Payments helps developers add Stripe payment flows to generated applications without putting Stripe secret keys in frontend code. Developers configure their own Stripe account, agents manage products and prices, and app frontends create Stripe Checkout and Billing Portal sessions through InsForge. Stripe remains the source of truth. InsForge mirrors Stripe catalog and runtime payment state into a payments schema so the dashboard, agents, and custom app logic can reason about products, prices, checkout sessions, subscriptions, refunds, and webhook processing.
InsForge does not decide what a successful payment means for your app. Use the payment projection tables to update your own app tables, such as orders, credits, team_entitlements, or billing_status.

Technology Stack

Core Model

InsForge uses the developer-owned Stripe account model. Each project can configure:
EnvironmentSecret keyPurpose
testSTRIPE_TEST_SECRET_KEYDevelopment and agent implementation
liveSTRIPE_LIVE_SECRET_KEYProduction payments
Test and live are independent targets. Every mirrored payment table stores an environment column instead of using separate test/live table sets.
Agents should default to Stripe test mode while building. Only target live after the developer explicitly approves production Stripe changes.

Configuration Flow

Configure Stripe from the dashboard Payments settings, or through the CLI:
npx @insforge/cli payments status
npx @insforge/cli payments config set test sk_test_xxx
npx @insforge/cli payments config set live sk_live_xxx
npx @insforge/cli payments sync
See Payments CLI for the full admin and agent command workflow. When a key is configured, InsForge validates it against Stripe, records the Stripe account identity, stores the secret in the InsForge secret store, best-effort configures the managed webhook, and syncs Stripe state when the account changes. If the backend URL is not publicly accessible, Stripe webhook creation can fail while key configuration still succeeds. Retry webhook setup after deploying to a public URL:
npx @insforge/cli payments webhooks configure test
npx @insforge/cli payments webhooks configure live

Catalog Management

Products and prices are managed in Stripe first, then mirrored into InsForge.
npx @insforge/cli payments products create \
  --environment test \
  --name "Pro Plan" \
  --description "Monthly access" \
  --idempotency-key "product:pro"

npx @insforge/cli payments prices create \
  --environment test \
  --product prod_123 \
  --currency usd \
  --unit-amount 1900 \
  --interval month \
  --idempotency-key "price:pro:monthly"
Stripe prices are immutable for amount, currency, and recurring cadence. To change those fields, create a new price and archive the old one.

Data Model

The payments schema stores Stripe connection metadata, catalog mirrors, checkout attempts, subscriptions, payment history, and webhook processing state.
TablePurpose
payments.stripe_connectionsOne row per environment with Stripe account, key, webhook, and sync status
payments.productsMirrored Stripe products
payments.pricesMirrored Stripe prices
payments.checkout_sessionsLocal Checkout Session attempts created by app frontends
payments.customer_portal_sessionsLocal Billing Portal Session attempts
payments.stripe_customer_mappingsMaps app billing subjects to Stripe customers
payments.subscriptionsMirrored Stripe subscription state
payments.subscription_itemsMirrored subscription item and price state
payments.payment_historyOne-time payments, subscription invoices, failed payments, and refunds
payments.webhook_eventsStripe webhook deduplication and processing status

Billing Subjects

A billing subject is the app-defined owner of a customer or subscription:
{ "type": "team", "id": "team_123" }
The subject can represent a user, team, organization, workspace, tenant, group, or any other app-specific billing owner. InsForge stores this as subject_type and subject_id; your app decides what those values mean.

Runtime Checkout Flow

Use the TypeScript SDK from your app frontend:
const { data, error } = await insforge.payments.createCheckoutSession({
  environment: 'test',
  mode: 'payment',
  lineItems: [{ stripePriceId: 'price_123', quantity: 1 }],
  successUrl: `${window.location.origin}/checkout/success`,
  cancelUrl: `${window.location.origin}/pricing`,
  customerEmail: user?.email ?? null,
  metadata: { order_id: orderId },
  idempotencyKey: `order:${orderId}`
});

if (error) throw error;
if (data?.checkoutSession.url) {
  window.location.assign(data.checkoutSession.url);
}
Checkout creation works in two steps:
  1. InsForge inserts a row in payments.checkout_sessions using the caller’s role and JWT context.
  2. InsForge creates the Stripe Checkout Session. If Stripe succeeds, the local row becomes open and stores the Stripe URL.
For one-time checkout, subject is optional. For subscription checkout, subject is required because ongoing access belongs to an app-defined billing owner.
await insforge.payments.createCheckoutSession({
  environment: 'test',
  mode: 'subscription',
  subject: { type: 'team', id: teamId },
  lineItems: [{ stripePriceId: 'price_monthly_123', quantity: 1 }],
  successUrl: `${window.location.origin}/billing/success`,
  cancelUrl: `${window.location.origin}/billing`,
  customerEmail: user.email,
  idempotencyKey: `team:${teamId}:pro-monthly`
});
Success URLs are for user experience only. Do not fulfill orders, grant credits, or activate subscriptions from the success page. Use webhook-projected payment state instead.

Customer Portal Flow

Use Stripe Billing Portal when customers need to manage subscriptions, invoices, payment methods, or cancellation.
const { data, error } = await insforge.payments.createCustomerPortalSession({
  environment: 'test',
  subject: { type: 'team', id: teamId },
  returnUrl: `${window.location.origin}/billing`
});

if (error) throw error;
if (data?.customerPortalSession.url) {
  window.location.assign(data.customerPortalSession.url);
}
Portal creation requires an authenticated user and an existing payments.stripe_customer_mappings row for the subject. That mapping is usually created after a Checkout Session completes and Stripe returns a customer.

Webhook Projection Flow

Managed Stripe webhooks update InsForge’s payment projections. The webhook handler verifies Stripe signatures, deduplicates events, and records processing state in payments.webhook_events. InsForge listens for events that keep checkout, subscriptions, payment history, and refunds current:
Event familyExamplesProjection
Checkoutcheckout.session.completed, checkout.session.async_payment_succeeded, checkout.session.expiredcheckout_sessions, customer mappings, one-time payment history
Invoicesinvoice.paid, invoice.payment_failedsubscription invoice payment history
PaymentIntentspayment_intent.succeeded, payment_intent.payment_failedpayment history
Refundscharge.refunded, refund.created, refund.updated, refund.failedrefund rows and original payment refund status
Subscriptionscustomer.subscription.created, customer.subscription.updated, customer.subscription.deletedsubscriptions and subscription items
Webhook delivery can be duplicated or out of order. InsForge processing is idempotent, and your custom fulfillment logic should be idempotent too. Stripe’s own guidance is the same: use webhooks for fulfillment, make fulfillment safe to run multiple times, don’t depend on event ordering, and return quickly from webhook handlers. See Stripe’s Checkout fulfillment guide and webhook best practices.

Custom Fulfillment Logic

InsForge cannot know what a successful payment means for every app. Your agent should create app-specific tables, triggers, or jobs based on your product model. Treat the payments schema as an internal projection layer, then copy the business result into your own public tables with RLS. For example, a SaaS app might turn a paid subscription into public.team_billing_status, while a marketplace might turn a one-time payment into public.orders.status = 'paid'. The frontend should read those app-owned tables, not the admin payment projection tables. If the frontend needs to be notified immediately, publish or subscribe from the app-owned table. For example, use Realtime on public.orders after the fulfillment trigger marks the order paid. Avoid subscribing end users directly to payments.payment_history.

Which Table Should Trigger Business Logic?

Use the most final payment projection available for the job:
Business needRecommended sourceWhy
Mark a one-time order paidpayments.payment_history where type = 'one_time_payment' and status = 'succeeded'Handles webhook confirmation and delayed payment methods
Apply refunds or reduce creditspayments.payment_history refund rows, or original rows with amount_refundedRefunds can arrive separately from the original payment
Activate or revoke subscription accesspayments.subscriptions status and period fieldsCaptures subscription lifecycle changes, renewals, cancellations, and sync repair
Record invoice/payment ledger entriespayments.payment_history where type = 'subscription_invoice'Captures paid and failed recurring invoices
Track local checkout attemptspayments.checkout_sessionsUseful for metadata, idempotency, and debugging, but not the final fulfillment signal
Do not fulfill directly from payments.checkout_sessions.status = 'completed'. Checkout completion is not always the same as money being available, especially with delayed payment methods. Use payment_history.status = 'succeeded' for one-time payment fulfillment and subscriptions.status for subscription access. Typical app-owned tables include:
Use caseApp-owned table
Ecommerce orderspublic.orders
Credit packspublic.credit_ledger
Paid contentpublic.user_entitlements
Team subscriptionspublic.team_billing_status
Async side effectspublic.fulfillment_jobs

One-Time Checkout Fulfillment Example

Create your order before Checkout, pass the order ID in Checkout metadata, then fulfill from webhook-projected payment history. The trigger below is intentionally idempotent: repeated webhook deliveries or manual sync repairs update the same order and do not double-fulfill it.
CREATE TABLE IF NOT EXISTS public.orders (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id),
  status TEXT NOT NULL DEFAULT 'pending'
    CHECK (status IN ('pending', 'paid', 'fulfilled', 'canceled', 'refunded')),
  stripe_checkout_session_id TEXT,
  stripe_payment_intent_id TEXT,
  paid_at TIMESTAMPTZ,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY "users read own orders"
ON public.orders
FOR SELECT
TO authenticated
USING (user_id = auth.uid());

CREATE OR REPLACE FUNCTION public.fulfill_paid_order()
RETURNS TRIGGER AS $$
BEGIN
  IF NEW.type <> 'one_time_payment' OR NEW.status <> 'succeeded' THEN
    RETURN NEW;
  END IF;

  WITH checkout_order AS (
    SELECT (cs.metadata->>'order_id')::uuid AS order_id
    FROM payments.checkout_sessions AS cs
    WHERE cs.stripe_checkout_session_id = NEW.stripe_checkout_session_id
      AND cs.metadata->>'order_id' ~* '^[0-9a-f-]{8}-[0-9a-f-]{4}-[0-9a-f-]{4}-[0-9a-f-]{4}-[0-9a-f-]{12}$'
  )
  UPDATE public.orders AS o
  SET
    status = 'paid',
    paid_at = COALESCE(NEW.paid_at, NOW()),
    stripe_checkout_session_id = NEW.stripe_checkout_session_id,
    stripe_payment_intent_id = NEW.stripe_payment_intent_id,
    updated_at = NOW()
  FROM checkout_order
  WHERE o.id = checkout_order.order_id
    AND o.status = 'pending';

  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER fulfill_paid_order
AFTER INSERT OR UPDATE OF status
ON payments.payment_history
FOR EACH ROW
EXECUTE FUNCTION public.fulfill_paid_order();
Frontend checkout code should create the pending order first and include the order ID in metadata:
const { data: order } = await insforge
  .from('orders')
  .insert([{ user_id: user.id, status: 'pending' }])
  .select()
  .single();

const { data, error } = await insforge.payments.createCheckoutSession({
  environment: 'test',
  mode: 'payment',
  lineItems: [{ stripePriceId: 'price_123', quantity: 1 }],
  successUrl: `${window.location.origin}/orders/${order.id}`,
  cancelUrl: `${window.location.origin}/pricing`,
  customerEmail: user.email,
  metadata: { order_id: order.id },
  idempotencyKey: `order:${order.id}`
});

if (error) throw error;
if (data?.checkoutSession.url) window.location.assign(data.checkoutSession.url);
The success page may show “processing” and poll the app-owned orders row, but it should not mark the order paid by itself. Stripe explicitly recommends webhooks because customers are not guaranteed to reach the success page after payment.

Subscription Entitlement Example

For subscriptions, update an app-owned entitlement or billing status table from payments.subscriptions.
CREATE OR REPLACE FUNCTION public.sync_team_billing_status()
RETURNS TRIGGER AS $$
BEGIN
  IF NEW.subject_type <> 'team'
     OR NEW.subject_id IS NULL
     OR NEW.subject_id !~* '^[0-9a-f-]{8}-[0-9a-f-]{4}-[0-9a-f-]{4}-[0-9a-f-]{4}-[0-9a-f-]{12}$' THEN
    RETURN NEW;
  END IF;

  INSERT INTO public.team_billing_status (
    team_id,
    stripe_subscription_id,
    status,
    current_period_end,
    updated_at
  )
  VALUES (
    NEW.subject_id::uuid,
    NEW.stripe_subscription_id,
    NEW.status,
    NEW.current_period_end,
    NOW()
  )
  ON CONFLICT (team_id) DO UPDATE SET
    stripe_subscription_id = EXCLUDED.stripe_subscription_id,
    status = EXCLUDED.status,
    current_period_end = EXCLUDED.current_period_end,
    updated_at = NOW();

  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER sync_team_billing_status
AFTER INSERT OR UPDATE OF status, current_period_end, cancel_at_period_end
ON payments.subscriptions
FOR EACH ROW
EXECUTE FUNCTION public.sync_team_billing_status();
In most SaaS apps, treat active and trialing as access-granting states, and treat canceled, unpaid, and incomplete_expired as access-revoking states. Stripe also recommends handling past_due deliberately: you might keep access during retry windows, show a billing warning, or restrict premium features based on your product policy.
Do not call external APIs directly from database triggers. For emails, shipping, webhooks, or other side effects, insert a row into an app-owned outbox table and process it from an edge function or background worker.

Agent Fulfillment Checklist

When an agent integrates Payments into an app, it should:
  1. Identify the app’s billing owner: user, team, organization, tenant, or anonymous order.
  2. Create app-owned read models such as orders, credit_ledger, or team_billing_status.
  3. Add RLS to app-owned tables before exposing them in the frontend.
  4. Add RLS to payments.checkout_sessions and payments.customer_portal_sessions when users can pass a subject.
  5. Add idempotent triggers from payments.payment_history or payments.subscriptions into the app-owned tables.
  6. Use app-owned table updates or Realtime messages to notify the frontend.
  7. Use outbox rows or edge functions for emails, shipping, analytics, or other external side effects.
  8. Test with Stripe test mode, including success, failed payment, delayed payment, renewal, cancellation, and refund paths.

Session Authorization and RLS

The runtime payment routes insert into payments.checkout_sessions and payments.customer_portal_sessions using the caller context. By default, the migration grants insert/select access for these session tables so apps can start quickly. If users can pass a subject, add app-specific RLS policies before shipping subscription checkout or portal UI. For example, if subscriptions belong to teams, only team members should be able to create checkout or portal sessions for that team.
ALTER TABLE payments.checkout_sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE payments.customer_portal_sessions ENABLE ROW LEVEL SECURITY;

CREATE POLICY "team members create checkout sessions"
ON payments.checkout_sessions
FOR INSERT
TO authenticated
WITH CHECK (
  subject_type = 'team'
  AND EXISTS (
    SELECT 1
    FROM public.team_members tm
    WHERE tm.team_id = subject_id::uuid
      AND tm.user_id = auth.uid()
  )
);

CREATE POLICY "team members create portal sessions"
ON payments.customer_portal_sessions
FOR INSERT
TO authenticated
WITH CHECK (
  subject_type = 'team'
  AND EXISTS (
    SELECT 1
    FROM public.team_members tm
    WHERE tm.team_id = subject_id::uuid
      AND tm.user_id = auth.uid()
  )
);
Adjust table names, casts, and membership checks to match your app schema.

Runtime Payment State

The frontend SDK currently exposes creation flows only:
  • insforge.payments.createCheckoutSession(...)
  • insforge.payments.createCustomerPortalSession(...)
Admin payment projections such as payments.subscriptions and payments.payment_history are not a generic end-user read surface. If your app needs users to see subscription status, order status, credits, or entitlements, create app-owned tables with RLS and populate them from payment projections.

Local Development

Stripe requires webhook endpoints to be publicly accessible. Localhost backend URLs cannot be registered as Stripe webhook endpoints from the dashboard or CLI. For local testing:
  1. Configure the Stripe test key.
  2. Build checkout with environment: 'test'.
  3. Use the Stripe CLI to forward events to your local backend webhook endpoint.
  4. Confirm your app-owned fulfillment tables update from webhook-projected payment state.

Current Limitations

Payments private preview intentionally starts with a small, stable surface.
LimitationDetails
Developer-owned Stripe accounts onlyConnected Accounts, claimable sandboxes, and platform-managed onboarding are not part of this phase
No test-to-live publishingAgents explicitly target test or live environments
No generic end-user read APIBuild app-owned read models with RLS for entitlements and billing status
No full Stripe object mirrorCustomers, invoices, charges, payment methods, and checkout line-item projections are intentionally limited
No default business fulfillmentAgents generate custom triggers/jobs based on your app schema

Best Practices

Start in Test Mode

Build and verify checkout with Stripe test keys before touching live data.

Use Idempotency Keys

Use stable keys for product, price, and checkout creation to avoid duplicates during retries.

Fulfill from Webhooks

Use payment projection tables, not success redirects, to grant access or mark orders paid.

Own Your App State

Store user-facing status in app-owned tables with RLS.

Protect Billing Subjects

Add RLS before exposing team, organization, or subscription management flows.

Keep Secrets Server-Side

Never put Stripe secret keys in frontend code or public deployment variables.

Payments CLI

Configure Stripe keys, sync catalog state, manage products and prices, and inspect projections

TypeScript

Create Checkout Sessions and Billing Portal Sessions from web applications