Skip to main content
Lemon Squeezy is the path of least resistance: the friendliest onboarding of any MoR, no business entity required, sign up and start charging in an afternoon. Fees are 5% + 50¢, similar to Paddle. They were acquired by Stripe in 2024, but the product still operates independently and the indie-hacker DX is intact. Pick Lemon Squeezy if you want to ship today, you’re a sole proprietor, or you just don’t want to deal with VAT/sales-tax compliance yet.

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

In src/config/site.ts:
payment: {
  provider: 'lemonsqueezy' as 'stripe' | 'paddle' | 'lemonsqueezy' | 'creem',
  currency: 'usd',
},

2. Create products and variants

Lemon Squeezy uses products with one or more variants. Variants are what you actually sell (e.g. “Vibestrap Promo 49"and"VibestrapStandard49" and "Vibestrap Standard 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 the priceId 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 into LEMON_SQUEEZY_STORE_ID.

4. Set env vars

In .env.local (variable names match src/env.ts):
LEMON_SQUEEZY_API_KEY=eyJ0eXAiOiJKV1Qi...      # Settings → API
LEMON_SQUEEZY_STORE_ID=12345
LEMON_SQUEEZY_WEBHOOK_SECRET=your-signing-secret

# vibestrap product variant IDs (numeric, paste as strings)
LEMON_VARIANT_VIBESTRAP_PROMO=678901
LEMON_VARIANT_VIBESTRAP_STANDARD=678902

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_SECRET in your env.
For local testing, expose localhost:3000 via ngrok or cloudflared.

Verify it works

  1. With test mode enabled in your Lemon Squeezy store, run pnpm dev.
  2. Open /pricing, click checkout — Lemon Squeezy’s hosted overlay opens.
  3. Pay with their test card: 4242 4242 4242 4242, any future expiry, any CVC.
  4. Watch the webhook arrive (check terminal logs).
  5. Check the database:
    select id, provider, scene, status, amount from payment
    where provider = 'lemonsqueezy'
    order by created_at desc limit 1;
    

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_ID is 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; calling paymentManager.createPortalLink throws by design — see src/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