Version pricing catalogs and grandfathered purchases - #1810
Conversation
…rchases Move Paddle and Polar pricing data into source-controlled sandbox and production catalogs with active and legacy offerings. Update the website to show the new five-plan catalog, retire cloud lifetime, use dynamic yearly savings, and resolve Polar product and price identities correctly. Persist provider identities on purchases and lazily backfill active legacy purchases so existing subscriptions and lifetime customers remain associated with their original provider pricing. Add the Drizzle-generated forward migration and update checkout, provisioning, and webhook handling without modifying historical migrations.
Add a standalone undated pricing promise page that explains how existing subscription and lifetime rates are protected as new pricing is introduced. Link the promise from the pricing section and shared footer, and prerender the route so the commitment is directly accessible.
…mise The versioned payment catalog on this branch raises cloud to $6/$50 and self hosted to $4/$35/$120, while keeping the legacy price IDs alive so existing subscribers keep renewing at the rate they signed up at. That behaviour is invisible to customers unless we say it out loud, and the /pricing-promise page is deliberately short and evergreen - it states the policy but carries no date, no numbers, and no reasoning. This adds the durable artifact for the change: a blog post that can be linked for years. It names the effective date (9 August 2026), lists the old and new amounts, explains why the original prices were set too low to sustain the project, and spells out the four rules that decide who keeps their rate - subscription start date before the cutoff, lifetime licenses untouched, cancel-and-return at the current price, new customers covered from their first payment. The post is written so it stays accurate after the change ships, so it avoids "subscribe today" framing and anchors every statement to the fixed date instead. Prices are listed as lists rather than a table because the MDX pipeline does not include remark-gfm and a table would render as literal text. Billing problems are routed to the home page contact form rather than a raw email address. No code or route changes are needed - the blog registry globs the content directory, so the file alone publishes at /blog/ryot-pricing-is-changing. Distribution to existing customers is handled separately.
Run the environment-aware legacy payment catalog backfill immediately after schema migrations so every existing purchase receives its provider price and product identity before renewal processing. Remove the lazy read-path backfill to avoid incomplete data and database writes during authenticated reads.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
WalkthroughThe change adds a shared Paddle and Polar payment catalog, stores provider identities on purchases, backfills legacy records, updates checkout and webhook flows, and publishes pricing-policy content with a new prerendered route. ChangesPayment Catalog and Pricing
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Customer
participant Pricing
participant me
participant PaymentProvider
participant Webhook
participant Provisioning
participant Database
Customer->>Pricing: Select plan
Pricing->>me: priceId, productType, planType
me->>PaymentProvider: Create purchase
PaymentProvider->>Webhook: Send completed transaction
Webhook->>Provisioning: Provider identity and plan data
Provisioning->>Database: Store purchase and provider identities
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/website/app/content/blog/ryot-pricing-is-changing/index.mdx`:
- Line 24: Use consistent hyphenation in
apps/website/app/content/blog/ryot-pricing-is-changing/index.mdx: change “Self
hosted” to “Self-hosted” at lines 24 and 40, “Self hosting” to “Self-hosting” at
line 46, “self hosting” to “self-hosting” at line 113, and “self host” to
“self-host” at line 115.
In `@apps/website/app/drizzle/migrations/0008_yielding_carnage.sql`:
- Around line 1-4: Rename the unapplied migration file from
0008_yielding_carnage.sql to the m<YYYYMMDD>_changes_for_issue_<number> format
and update its corresponding migration journal tag to match. If the migration is
already applied, keep the existing recorded name and do not rewrite migration
history.
- Line 4: Update migration 0008 to run without a transaction and create
customer_purchase_provider_lookup_idx using CREATE INDEX CONCURRENTLY, while
preserving the existing indexed columns. Keep the corresponding Drizzle schema
index declaration and generated metadata synchronized with this migration.
In `@apps/website/app/drizzle/schema.server.ts`:
- Around line 76-78: Reorder the fields in the payment-related schema definition
by ascending line length: place providerPriceId first, providerProductId second,
and paymentProvider last. Do not change their names, types, or column mappings.
In `@apps/website/app/lib/payment-catalog.ts`:
- Line 40: The object fields in payment-catalog.ts are not ordered by ascending
line length. Update the payment metadata return near PaymentPrice to place
priceId before productId, reorder PAYMENT_CATALOG keys with polar before paddle,
and place trial before amount in every listed pricing metadata object.
- Around line 29-40: Remove the explicit PaymentPrice return type annotations
from paddlePrice and polarPrice, and remove the redundant return type annotation
from getPaymentEnvironment. Preserve their existing parameters, defaults, and
returned object values so TypeScript infers the same types.
In `@apps/website/app/lib/utilities.server.ts`:
- Around line 51-52: In the return object within the price-matching logic, and
the corresponding return object around the second referenced location, reorder
the fields so planType appears before productType, placing the shorter property
line first while preserving values and behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1be56577-390f-4c1c-b38d-03e6c210145a
📒 Files selected for processing (19)
apps/website/app/content/blog/ryot-pricing-is-changing/index.mdxapps/website/app/drizzle/migrations/0008_yielding_carnage.sqlapps/website/app/drizzle/migrations/meta/0008_snapshot.jsonapps/website/app/drizzle/migrations/meta/_journal.jsonapps/website/app/drizzle/schema.server.tsapps/website/app/lib/components/Pricing.tsxapps/website/app/lib/config.server.tsapps/website/app/lib/migrations.server.tsapps/website/app/lib/payment-catalog.tsapps/website/app/lib/pricing-config.tsapps/website/app/lib/provisioning.server.tsapps/website/app/lib/utilities.server.tsapps/website/app/root.tsxapps/website/app/routes/health.tsxapps/website/app/routes/me.tsxapps/website/app/routes/paddle-webhook.tsxapps/website/app/routes/polar-webhook.tsxapps/website/app/routes/pricing-promise.tsxapps/website/react-router.config.ts
💤 Files with no reviewable changes (1)
- apps/website/app/lib/pricing-config.ts
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/website/app/routes/polar-webhook.tsx (1)
59-60: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject orders without a Polar
productPriceId.
getProductAndPlanTypeByPolarIdsmatches byproductIdalone whenpriceIdis absent, andhandlePurchaseOrRenewalstoresproviderPriceIdasundefined. Add a requiredproductPriceIdcheck so each Polar purchase resolves the intended price and preserves the provider price identity.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/website/app/routes/polar-webhook.tsx` around lines 59 - 60, In the webhook handler before calling getProductAndPlanTypeByPolarIds, require order.items[0].productPriceId to be present and reject or fail the order when it is missing. Pass the validated productPriceId to getProductAndPlanTypeByPolarIds so handlePurchaseOrRenewal receives and stores the intended provider price identity.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/website/app/drizzle/schema.server.ts`:
- Around line 76-78: Reorder the fields in the schema definition so the shorter
id declaration appears before paymentProvider, while leaving productType and all
field definitions unchanged.
In `@apps/website/app/lib/payment-catalog.ts`:
- Line 40: Update the object returned by the payment catalog mapping expression
so its fields are ordered by ascending line length, placing priceId before
productId while preserving name and metadata behavior.
In `@apps/website/app/routes/paddle-webhook.tsx`:
- Around line 187-189: Update the handler result status logic in
apps/website/app/routes/paddle-webhook.tsx lines 187-189 so every result.error
returns a non-2xx response, using a retryable status for transient lookup
failures rather than acknowledging them with 200. In
apps/website/app/routes/polar-webhook.tsx lines 148-153, replace error-text
matching with the same structured result status contract; update both webhook
route handlers consistently while preserving successful responses.
- Around line 129-131: Reorder the declarations in the webhook handler so the
variables appear by ascending declaration line length: place requestBody first,
paddleClient second, and serverVariables last.
---
Outside diff comments:
In `@apps/website/app/routes/polar-webhook.tsx`:
- Around line 59-60: In the webhook handler before calling
getProductAndPlanTypeByPolarIds, require order.items[0].productPriceId to be
present and reject or fail the order when it is missing. Pass the validated
productPriceId to getProductAndPlanTypeByPolarIds so handlePurchaseOrRenewal
receives and stores the intended provider price identity.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: d6d73e26-92a2-4305-8d31-4d26d0fbb488
📒 Files selected for processing (5)
apps/website/app/content/blog/ryot-pricing-is-changing/index.mdxapps/website/app/drizzle/schema.server.tsapps/website/app/lib/payment-catalog.tsapps/website/app/routes/paddle-webhook.tsxapps/website/app/routes/polar-webhook.tsx
Introduce versioned pricing catalogs while preserving grandfathered purchase rates for existing customers. Update the website to reflect new pricing plans, add a pricing promise page, and ensure legacy purchases maintain their original pricing identities. Implement necessary database migrations and backfill legacy purchase data to ensure continuity for existing subscriptions.
Summary by CodeRabbit
New Features
Bug Fixes
Style