Skip to content

Payment data model

Keep checkout intent, received provider events, and application entitlements in separate tables. This prevents a browser redirect from granting access and gives webhook retries a durable idempotency key.

Add these tables to pbvex/schema.ts alongside any existing application tables:

ts
import { defineSchema, defineTable } from 'pbvex/server';
import { v } from 'pbvex/values';

export default defineSchema({
  paymentCheckoutSessions: defineTable({
    owner: v.string(),
    plan: v.literal('premium'),
    status: v.union(v.literal('pending'), v.literal('completed')),
    providerCheckoutId: v.optional(v.string()),
  }).index('by_owner', ['owner']),

  billingAccounts: defineTable({
    owner: v.string(),
    plan: v.union(v.literal('free'), v.literal('premium')),
    status: v.union(v.literal('inactive'), v.literal('active')),
    checkoutSessionId: v.id('paymentCheckoutSessions'),
    activatedAt: v.number(),
  }).index('by_owner', ['owner']),

  paymentEvents: defineTable({
    providerEventId: v.string(),
    checkoutSessionId: v.id('paymentCheckoutSessions'),
    type: v.literal('checkout.completed'),
    receivedAt: v.number(),
  }).index('by_provider_event', ['providerEventId']),
});

Then regenerate the typed data model and function references:

bash
pbvex codegen

Table responsibilities

TableResponsibilityImportant invariant
paymentCheckoutSessionsRecords what an authenticated owner intends to buy and the external checkout that fulfills itThe server derives owner; the client never supplies it
paymentEventsRecords accepted provider deliveriesproviderEventId is checked before applying an event
billingAccountsStores the entitlement used by application functionsOnly trusted webhook processing activates it

billingAccounts.by_owner loads the current entitlement. paymentEvents.by_provider_event makes repeated webhook delivery safe. paymentCheckoutSessions.by_owner supports account history and cleanup of abandoned sessions.

providerCheckoutId is optional until the external create-checkout request succeeds. The webhook must match both this provider ID and the local checkout reference before granting access.

Continue with Generate checkout.

Generated API reference. Source of truth is the codebase.