Problem
StripeSubscriptions.createCheckoutSession (src/client/index.ts:166) accepts only a narrow subset of Stripe's Checkout.SessionCreateParams. Several commonly-used fields are silently unavailable:
allow_promotion_codes — enable user-facing promotion code input at Checkout
discounts — pre-apply a coupon or promotion code programmatically
automatic_tax — Stripe Tax integration
shipping_address_collection / billing_address_collection — for physical goods or compliance
ui_mode (embedded / hosted) — embedded Checkout
customer_creation, customer_update, payment_method_types, locale, expires_at, ...
This is the classic leaky-abstraction trap: the wrapper covers 20% of the underlying surface area, and any real product eventually needs something outside that 20%. Today it's promo codes; tomorrow it's tax or embedded mode.
What consumers are forced to do
To enable allow_promotion_codes for internal-testing promo codes, we had to:
- Add
stripe as a direct dependency (it was only transitive via @convex-dev/stripe).
- Construct a fresh
Stripe SDK instance in application code and call stripe.checkout.sessions.create() directly — duplicating the param-shape the component already builds internally.
- Audit the component to confirm that bypassing the session-creation wrapper doesn't corrupt component state.
The audit outcome is actually encouraging and worth documenting: the component's tables (customers, checkout_sessions, payments, subscriptions, invoices) are populated entirely by webhook events routed through registerStripeRoutes — not by the code path that created the session. So bypassing createCheckoutSession is safe for state integrity, as long as webhooks are still registered. That's a real strength of the design, but it makes the narrow wrapper feel even less justified: the component already does the right thing regardless of who creates the session.
Proposals (in order of minimal change)
Option A — pass-through escape hatch
createCheckoutSession(ctx, {
priceId, mode, successUrl, cancelUrl,
extraParams?: Partial<Stripe.Checkout.SessionCreateParams>,
})
Merge extraParams into sessionParams before calling the SDK. Keeps the convenience for the 80% case, unblocks the 20% case. Lowest-risk change.
Option B — accept full SDK shape
Accept Stripe.Checkout.SessionCreateParams directly, with the component's convenience fields (priceId → line_items, mode, metadata wiring) layered on top as optional builder sugar.
Option C — expose the SDK instance
Add a getStripe() accessor on StripeSubscriptions so consumers can run arbitrary Stripe SDK calls without re-instantiating (and without being forced to add stripe as a direct dep).
Options A and C are additive and non-breaking; B is a larger refactor. A + C together cover most real-world escape hatches with minimal surface.
Same critique applies to createCustomerPortalSession, cancelSubscription, etc. — any wrapper that hides the Stripe params will hit this eventually. A consistent extraParams + SDK accessor convention across the client would future-proof the component.
Version
@convex-dev/stripe@0.1.4
Happy to send a PR for Option A + C if there's agreement on direction.
Problem
StripeSubscriptions.createCheckoutSession(src/client/index.ts:166) accepts only a narrow subset of Stripe'sCheckout.SessionCreateParams. Several commonly-used fields are silently unavailable:allow_promotion_codes— enable user-facing promotion code input at Checkoutdiscounts— pre-apply a coupon or promotion code programmaticallyautomatic_tax— Stripe Tax integrationshipping_address_collection/billing_address_collection— for physical goods or complianceui_mode(embedded/hosted) — embedded Checkoutcustomer_creation,customer_update,payment_method_types,locale,expires_at, ...This is the classic leaky-abstraction trap: the wrapper covers 20% of the underlying surface area, and any real product eventually needs something outside that 20%. Today it's promo codes; tomorrow it's tax or embedded mode.
What consumers are forced to do
To enable
allow_promotion_codesfor internal-testing promo codes, we had to:stripeas a direct dependency (it was only transitive via@convex-dev/stripe).StripeSDK instance in application code and callstripe.checkout.sessions.create()directly — duplicating the param-shape the component already builds internally.The audit outcome is actually encouraging and worth documenting: the component's tables (
customers,checkout_sessions,payments,subscriptions,invoices) are populated entirely by webhook events routed throughregisterStripeRoutes— not by the code path that created the session. So bypassingcreateCheckoutSessionis safe for state integrity, as long as webhooks are still registered. That's a real strength of the design, but it makes the narrow wrapper feel even less justified: the component already does the right thing regardless of who creates the session.Proposals (in order of minimal change)
Option A — pass-through escape hatch
Merge
extraParamsintosessionParamsbefore calling the SDK. Keeps the convenience for the 80% case, unblocks the 20% case. Lowest-risk change.Option B — accept full SDK shape
Accept
Stripe.Checkout.SessionCreateParamsdirectly, with the component's convenience fields (priceId → line_items,mode, metadata wiring) layered on top as optional builder sugar.Option C — expose the SDK instance
Add a
getStripe()accessor onStripeSubscriptionsso consumers can run arbitrary Stripe SDK calls without re-instantiating (and without being forced to addstripeas a direct dep).Options A and C are additive and non-breaking; B is a larger refactor. A + C together cover most real-world escape hatches with minimal surface.
Same critique applies to
createCustomerPortalSession,cancelSubscription, etc. — any wrapper that hides the Stripe params will hit this eventually. A consistentextraParams+ SDK accessor convention across the client would future-proof the component.Version
@convex-dev/stripe@0.1.4Happy to send a PR for Option A + C if there's agreement on direction.