EasyStarter defaults to three plans: Free, Pro (monthly + yearly), and Lifetime (one-time purchase).
The Free plan requires no Creem product. Follow these steps to create Pro and Lifetime:
Go to Products in your Creem dashboard and click Create product
Enter a product name, e.g. Pro Monthly
Subscription plans (monthly / yearly): set the billing type to Recurring — create one product for monthly and one for yearly, each with its own billing cycle
Lifetime plan: set the billing type to One time — create one product
After saving each product, copy its Product ID (format: prod_xxxxxxx) — you'll use these in the next step
Add the Product IDs from the previous step to the web.payments.plans field in packages/app-config/src/app-config.ts:
packages/app-config/src/app-config.ts
web: { payments: { provider: "creem", // test/prod hold environment-specific Creem Product IDs. // Use test product IDs with creem_test_, and live product IDs with creem_live_. plans: [ { id: "free", // Free plan — no Product ID required }, { id: "pro", prices: [ { id: "monthly", provider: "creem", test: { providerPriceId: "prod_xxxxxxxxxxxxxxxx", // sandbox test Creem monthly Product ID }, prod: { providerPriceId: "prod_xxxxxxxxxxxxxxxx", // production Creem monthly Product ID }, currency: "usd", amountCents: 1000, // $10.00 priceType: "subscription", interval: "month", trialDays: 7, // Must match the trial days set on the Creem product status: "active", }, { id: "yearly", provider: "creem", test: { providerPriceId: "prod_xxxxxxxxxxxxxxxx", // sandbox test Creem yearly Product ID }, prod: { providerPriceId: "prod_xxxxxxxxxxxxxxxx", // production Creem yearly Product ID }, currency: "usd", amountCents: 10000, // $100.00 priceType: "subscription", interval: "year", trialDays: 7, // Must match the trial days set on the Creem product status: "active", }, ], }, { id: "lifetime", prices: [ { id: "lifetime", provider: "creem", test: { providerPriceId: "prod_xxxxxxxxxxxxxxxx", // sandbox test Creem one-time Product ID }, prod: { providerPriceId: "prod_xxxxxxxxxxxxxxxx", // production Creem one-time Product ID }, currency: "usd", amountCents: 20000, // $200.00 priceType: "lifetime", status: "active", }, ], }, ], },},
Field reference:
Field
Description
test.providerPriceId
Creem Product ID from the test environment, format prod_xxx
prod.providerPriceId
Creem Product ID from the production environment, format prod_xxx
amountCents
Price in cents — 1000 = $10.00
priceType
"subscription" for recurring, "lifetime" for one-time
interval
Billing cycle: "month" or "year" (omit for lifetime)
trialDays
Must match the trial days configured on the Creem product — Creem does not support setting trial periods via API, so this value is display-only and must stay in sync with the product setting in the Creem dashboard
status
"active" to show / "archived" to hide from the pricing page
Webhooks are how Creem notifies your server about events like successful payments, subscription changes, and refunds. They are essential for keeping your database in sync.
In your Creem dashboard, go to Webhooks
Click Add endpoint
Set the Endpoint URL:
Local development: https://your-ngrok-url/api/webhooks/creem (use ngrok or similar tunnel)
EasyStarter includes Creem Customer Billing Portal integration out of the box. Users can manage their subscriptions from /settings/billing, where the app creates a portal session via the Creem API and redirects them to Creem's hosted management UI.
EasyStarter's payment layer is built around a PaymentProvider interface with Stripe and Creem as built-in implementations. You can swap in any other provider (e.g. Paddle, LemonSqueezy) in six steps without touching any business logic.
import type { Database } from "@/db";export async function handlePaddleEvent(db: Database, payload: unknown) { const event = payload as { event_type: string; data: unknown }; switch (event.event_type) { case "subscription.created": case "subscription.updated": case "subscription.canceled": { // Sync subscription state into the billing_subscription table break; } case "transaction.completed": { // Handle one-time purchases and write to billing_purchase table break; } // Handle other events as needed... default: break; }}
See apps/server/src/payments/providers/stripe/webhook/ for how to split complex event types across multiple files.
Once complete, all checkout sessions, upgrades, and portal redirects will go through the new provider automatically — no changes to business logic needed.