Alt description missing in image
Beta: Plugins coming soon!
Alt description missing in image

ensureCustomer()

import { ensureCustomer } from '@payments/driver'

Finds or creates a payment provider Customer for the given user, then ensures a corresponding Customer record exists in your DB. This universal function delegates to your active payment driver implementation (e.g. Stripe, Polar, RevenueCat) as configured in @app/config.

The driver automatically uses the payment provider you’ve selected in appConfig.ts (e.g. 'stripe', 'polar', etc.).

const { user, customer } = await ensureCustomer({
    user, // Can be partial, must at least include 'email' and 'userId' fields
    customerOverrides: { name: 'Jane Doe', email: 'jane@example.com' },
    metadata: { onboardingStep: 'billing' },
})

Implementation details

Each payment driver implementation (like @payments/stripe) handles the provider-specific logic:

  • Validates user.userId and user.email are present
  • Attempts to find an existing customer in the payment provider by email; otherwise creates one with inferred name and metadata
  • If newly created, updates your Users model with the provider’s customer ID (e.g. stripeCustomerId)
  • Upserts a Customer record in your DB linking userIdcustomerId with the provider type

The exact name inference logic may vary by provider, but typically checks:

  • user.fullNameuser.nameuser.firstNameuser.username → guessed from email
packages/@payments-driver/index.ts
export const ensureCustomer = paymentsDriver['ensureCustomer']

The driver entrypoint forwards calls to the active implementation from @app/registries/drivers/payments.drivers.generated.ts.

EnsureCustomerInput

The underlying driver implementations will follow the driver signature EnsureCustomerInput schema:

import { EnsureCustomerInput } from '@payments/driver/driver.signature'
export const EnsureCustomerInput = schema('EnsureCustomerInput', {
    user: User,
    customerOverrides: z.object({
        email: Customer.shape.email.nullish().describe('Override the email on the Customer record for this checkout session'),
        name: Customer.shape.name.nullish().describe('Override the name on the Customer record for this checkout session'),
        address: z.object({
            line1: z.string().nullish().describe('Address line 1'),
            city: z.string().nullish().describe('City'),
            state: z.string().nullish().describe('State/Province'),
            postalCode: z.string().nullish().describe('Postal/ZIP code'),
            country: z.string().nullish().describe('Country code (ISO 3166-1 alpha-2)'),
        }).nullish().describe('Override the address on the Customer record for this checkout session'),
        taxId: z.string().nullish().describe('Override the tax ID on the Customer record for this checkout session'),
    }).nullish().describe('Object containing fields to override on the Customer record for this checkout session'),
    metadata: z.record(z.string(), z.any()).nullish().describe('Optional metadata to attach to the checkout session'),
})

EnsureCustomerOutput

The underlying driver implementations will follow the driver signature EnsureCustomerOutput schema:

import { EnsureCustomerOutput } from '@payments/driver/driver.signature'
export const EnsureCustomerOutput = schema('EnsureCustomerOutput', {
    provider: PaymentProvidable.shape.provider, // e.g. 'stripe' | 'polar' | 'revenuecat'
    user: User,
    customer: Customer,
})

Recommendations

💡

It’s good practice to call ensureCustomer() as a prerequisite to startCheckout() and startPortalSession() calls. This ensures you always have a single customer in your payment provider for each user email.

Most payment driver implementations (like Stripe) call ensureCustomer() internally within startCheckout() and startPortalSession() for convenience, but calling it explicitly gives you more control.