Skip to content

Move billing to the shared billing engine with Lemon Squeezy - #207

Merged
rajat1saxena merged 6 commits into
mainfrom
billing
Oct 9, 2026
Merged

rajat1saxena merged 6 commits into
mainfrom
billing

Conversation

@rajat1saxena

Copy link
Copy Markdown
Member

Summary

Moves MediaLit's subscriptions onto the shared billing engine (@codelitdev/billing and @codelitdev/platform 0.4.1). The engine supports both Lemon Squeezy and Dodo. Today's Lemon Squeezy subscribers keep working, and a new billing:adopt script links them to their MediaLit accounts.

What changes

Billing engine

  • Subscription state now lives in the billing tables. The old subscription fields on profiles are dropped by migration 0007_drop_legacy_subscription. Paid limits come from the engine's entitlements.
  • BILLING_PROVIDER chooses where new checkouts go (lemonsqueezy or dodo). Every provider whose credentials are set stays connected, so its existing subscribers can still renew, cancel and open the portal.
  • Webhooks now go to the API at <PUBLIC_API_URL>/webhooks/billing/<provider>. The Dodo-only webhook route in the web app and its old checkout, crypto and authorization code are removed.
  • Cancelling, resuming and opening the portal require a sign-in from the last 15 minutes (BILLING_RECENT_AUTH_MAX_AGE_SECONDS). Login supports ?reauth=1&next=... to return the user to billing, and next accepts only same-site paths.

Billing page

  • Shows Basic and Pro plans with a Monthly/Yearly toggle, and a checkout for the selected billing period.
  • Subscribers get Manage billing (the provider portal), Cancel (takes effect at the end of the billing period) and Resume.

Adopting existing Lemon Squeezy subscriptions

  • bun --filter @medialit/api billing:adopt lists the store's live subscriptions and matches each to a MediaLit account by email. It's a dry run unless given -- --apply --operator <email>.
  • It skips subscriptions that have ended, belong to a product outside the catalog, or have no matching account.

Self-hosting

  • Docker Compose now defaults to MEDIALIT_DEPLOYMENT_MODE=oss: no billing, and no upload or storage limits.
  • The MongoDB import no longer copies v0.4.0 subscription fields. The self-hosting and v0.4.0 to v0.5.0 upgrade docs explain this.

Dependencies

  • All @codelitdev/* packages are now on 0.4.1.

Deploying to cloud

  1. Set the new variables from apps/api/.env.example:
    • BILLING_PROVIDER
    • BILLING_PRO_MONTH_PRODUCT_ID and BILLING_PRO_YEAR_PRODUCT_ID (Lemon Squeezy variant IDs)
    • BILLING_DATA_ENCRYPTION_KEY
    • LEMONSQUEEZY_API_KEY, LEMONSQUEEZY_STORE_ID and LEMONSQUEEZY_WEBHOOK_SECRET
    • Raise BILLING_CATALOG_REVISION if the provider or product IDs changed.
  2. Run the migrations.
  3. Point the Lemon Squeezy store's webhook at <PUBLIC_API_URL>/webhooks/billing/lemonsqueezy.
  4. Turn off plan changes in the Lemon Squeezy customer portal. The engine doesn't support changing plans in the portal, and it quarantines plan changes it didn't start.
  5. Run billing:adopt as a dry run, review its report, then run it with --apply --operator <email>.

Testing

  • API typecheck, the billing drift check and 91 API tests pass. The web and docs apps typecheck.
  • I tested the whole flow in a browser against a Lemon Squeezy test-mode store, first with locally built packages and then with the published 0.4.x packages:
    • Checkout: only the chosen variant is shown, and switching between monthly and yearly starts a fresh checkout. Paying with the test card activates the subscription through the webhook.
    • Subscription management: cancel and resume, with MediaLit and Lemon Squeezy matching after each step. Manage billing opens the customer's portal, including after Lemon Squeezy moved the subscription to a new customer.
    • Plan changes: monthly to yearly and back, more than once. A do_not_bill change between billing periods is refused.
    • Failed payment: the declining test card puts the subscription in past_due. Once a cancelled past_due subscription expires, the account returns to Basic.
    • Adoption: a billing:adopt dry run.

Known limitations

  • billing:adopt reports an already-adopted subscription as "would adopt", and during --apply one refused subscription stops the run.
  • The billing page always opens on the Monthly tab, even for yearly subscribers, and shows no notice while a payment is past due.

Rajat added 4 commits October 8, 2026 20:39
Move subscription state to the shared billing system, add plan
management
and recent-auth billing actions, and remove legacy subscription fields.
…tions

Compose the shared billing engine with Lemon Squeezy or Dodo, chosen by
BILLING_PROVIDER, while every connected provider keeps handling
webhooks, cancel, resume and the portal. Replace the Dodo-only checkout
and webhook route with the shared provider setup.

Add billing:adopt, which links Lemon Squeezy subscriptions started
before the shared engine to MediaLit accounts. It is a dry run unless
--apply --operator is given.

Requires @codelitdev/billing and @codelitdev/platform 0.4.0. bun.lock
is updated once that release is published.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-09T05:35:39.389921Z ab7a371 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Comment thread apps/web/app/login/page.tsx Fixed
Comment thread apps/web/app/login/page.tsx Fixed
The next path was checked as a string, but browsers drop tabs and
newlines from URLs, so /<tab>/evil.com passed and became //evil.com.
Resolve it as a URL and accept it only on the same origin, in both
the login page and the proxy.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab7a371d1b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/api/src/app.ts
Comment thread apps/api/src/mcp/tools/media.ts
The MCP tools now read maxStorage from the billing engine. The local
stack runs the API in cloud mode with an unusual Basic storage limit,
and the MCP suite checks that the test account, which has no
subscription, gets exactly that limit.
@rajat1saxena
rajat1saxena merged commit 87ee859 into main Oct 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants