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.userIdanduser.emailare 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
Usersmodel with the provider’s customer ID (e.g.stripeCustomerId) - Upserts a
Customerrecord in your DB linkinguserId↔customerIdwith the provider type
The exact name inference logic may vary by provider, but typically checks:
user.fullName→user.name→user.firstName→user.username→ guessed fromemail
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.
