Prerequisites
- A Lemon Squeezy account (lemonsqueezy.com) — signup is instant, and you can take real money in test mode immediately.
- A store created (Settings → Stores → New store). The store ID matters — you’ll need it.
- Postgres up, schema pushed.
1. Set the active provider
Insrc/config/site.ts:
2. Create products and variants
Lemon Squeezy uses products with one or more variants. Variants are what you actually sell (e.g. “Vibestrap Promo 99” are two variants of one product, OR two products with one variant each — either works). Go to Products → New product. Each variant has a numeric ID — copy these, they go into env vars. Important: vibestrap’s Lemon Squeezy provider expects thepriceId to be a
numeric variant ID (parsed via parseInt), not a string product slug.
3. Find your store ID
Settings → Stores → [your store]. The URL contains a number; that’s your store ID. Paste it intoLEMON_SQUEEZY_STORE_ID.
4. Set env vars
In.env.local (variable names match src/env.ts):
5. Configure the webhook
Settings → Webhooks → New webhook:- URL:
https://your-domain.com/api/webhooks/lemonsqueezy - Events (minimum):
order_created,subscription_created,subscription_updated,subscription_cancelled,subscription_expired. - Signing secret: pick any random string and paste it here AND into
LEMON_SQUEEZY_WEBHOOK_SECRETin your env.
localhost:3000 via ngrok or cloudflared.
Verify it works
- With test mode enabled in your Lemon Squeezy store, run
pnpm dev. - Open
/pricing, click checkout — Lemon Squeezy’s hosted overlay opens. - Pay with their test card:
4242 4242 4242 4242, any future expiry, any CVC. - Watch the webhook arrive (check terminal logs).
- Check the database:
Common pitfalls
- Store ID lives per-account, not per-store — if you have multiple stores
in one account, you must explicitly choose one. The provider’s checkout call
fails fast with a clear error if
LEMON_SQUEEZY_STORE_IDis unset. - Variants vs products confusion — pricing pages often link to products,
but checkout requires variant IDs. If the URL on the dashboard contains
/products/123, that’s a product ID, not a variant ID. Click into a variant to find its ID. - No customer portal API — Lemon Squeezy issues each customer’s portal URL
at checkout time and includes it in the order webhook
(
customer.urls.customer_portal). You must store and reuse it; callingpaymentManager.createPortalLinkthrows by design — seesrc/payment/provider/lemonsqueezy.ts. - Webhook secret is whatever you typed — unlike Stripe, Lemon Squeezy
doesn’t generate the signing secret for you. Pick something long and random
(
openssl rand -hex 32). - Test mode order IDs reset — if you delete a webhook and recreate it, old test events won’t replay. Issue a new test purchase instead.
Official docs
- Lemon Squeezy Docs: docs.lemonsqueezy.com
- API reference: docs.lemonsqueezy.com/api
- Webhooks guide: docs.lemonsqueezy.com/help/webhooks
- Test mode: docs.lemonsqueezy.com/help/getting-started/test-mode