Creem
Creem checkout and webhook provider for the TanStack Start template.
Creem is the default provider. It implements the shared
PaymentProvider interface in
src/lib/payment/providers/creem.ts: it creates hosted checkouts, verifies
webhook signatures, and handles a couple of lifecycle events.
Configuration
| Variable | Where | Purpose |
|---|---|---|
VITE_PAYMENT_PROVIDER | Build-time | Set to creem (the default) to select this provider. |
CREEM_API_KEY | Worker secret | Server key for creating checkouts. |
CREEM_SERVER_IDX | Worker secret | Selects the Creem server (sandbox vs. live). Defaults to 0. |
CREEM_WEBHOOK_SECRET | Worker secret | HMAC secret for verifying webhook signatures. |
VITE_CREEM_PRODUCT_PRO_MONTHLY / _PRO_YEARLY / _LIFETIME | Build-time | Public Creem product IDs for the three plans. |
Set the server values with wrangler secret put CREEM_API_KEY (and the others).
See Environment Variables.
Prices vs. product IDs
src/config/price-config.ts holds the display prices. Each plan's
productId comes from a VITE_CREEM_PRODUCT_* variable and is what Creem
actually charges for. The display amount and the Creem product are independent —
keep them consistent yourself.
Checkout
createCheckout builds a Creem checkout for the requested productId and
returns its URL. The create route redirects the browser there, with success URL
{origin}/pay-success?orderId={orderNo}.
Webhook
- Path:
POST /api/payment/notify/creem(served by the shared/api/payment/notify/$providerroute). - Signature: HMAC-SHA256 of the raw body using
CREEM_WEBHOOK_SECRET, compared against the hexcreem-signatureheader with a constant-time check. A missing/invalid signature makes the route answer400. - Handled events:
checkout.completed— marks the matchingpaymentrowsuccessand stores the Creem order/customer IDs.- Subscription lifecycle —
subscription.active,subscription.paid,subscription.update,subscription.trialing,subscription.paused,subscription.past_due,subscription.scheduled_cancel,subscription.canceled, andsubscription.expiredupsert the user'ssubscriptionrow to the matching status, period, and cancel timestamps. refund.created— marks the matchingpaymentrowrefunded.
Local testing
- Use the Creem sandbox server (
CREEM_SERVER_IDX) and sandbox product IDs. - Expose your local webhook with a tunnel (e.g.
cloudflared/ngrok) and registerhttps://<tunnel>/api/payment/notify/creemin Creem. - Confirm the
paymentrow flips tosuccessaftercheckout.completed.
Production notes
- Switch to the live Creem server and live product IDs (rebuild for the
VITE_*IDs to take effect). - Webhooks can be retried; the handlers are not idempotent, so add event-id dedup if you extend them with non-terminal side effects.
Verify
- Pricing CTA creates a checkout and redirects to Creem.
- After paying in sandbox, the success page shows the order.
- The webhook marks the
paymentrowsuccess. - A subscription product creates/updates the
subscriptionrow.
