Skip to content

Version pricing catalogs and grandfathered purchases - #1810

Merged
IgnisDa merged 12 commits into
mainfrom
update-prices
Aug 9, 2026
Merged

Version pricing catalogs and grandfathered purchases#1810
IgnisDa merged 12 commits into
mainfrom
update-prices

Conversation

@IgnisDa

@IgnisDa IgnisDa commented Aug 9, 2026

Copy link
Copy Markdown
Owner

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

    • Added a Pricing Promise page covering price protection, lifetime licenses, renewals, and payment failures.
    • Added a blog post announcing upcoming Ryot Pro pricing changes and protections for existing subscribers.
    • Expanded pricing options with clearer trial savings information.
    • Improved checkout and purchase processing across supported payment providers.
  • Bug Fixes

    • Improved payment notification validation and error handling.
    • Preserved recognition of existing purchases during payment and plan updates.
    • Added clearer synchronization messaging with refresh guidance.
  • Style

    • Improved responsive footer layout and updated Terms contact information.

IgnisDa added 6 commits August 8, 2026 21:49
…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.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ddb5cb9d-abbe-4a37-bb72-bb3e464c49b4

📥 Commits

Reviewing files that changed from the base of the PR and between ed7af5c and 4e0a892.

📒 Files selected for processing (1)
  • apps/website/app/routes/terms.tsx

Walkthrough

The 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.

Changes

Payment Catalog and Pricing

Layer / File(s) Summary
Catalog and pricing flow
apps/website/app/lib/payment-catalog.ts, apps/website/app/lib/config.server.ts, apps/website/app/lib/utilities.server.ts, apps/website/app/lib/components/Pricing.tsx, apps/website/app/routes/me.tsx
Active and legacy Paddle and Polar catalogs now provide environment-aware products, prices, trials, and plan metadata. Pricing callbacks pass product and plan types to checkout.
Purchase identity schema
apps/website/app/drizzle/schema.server.ts, apps/website/app/drizzle/migrations/*
Purchases now store payment-provider, provider-price, and provider-product identities. Migration metadata and a composite lookup index were added.
Provider-aware provisioning
apps/website/app/lib/provisioning.server.ts, apps/website/app/routes/paddle-webhook.tsx, apps/website/app/routes/polar-webhook.tsx
Purchase and renewal provisioning persists provider metadata received from Paddle and Polar webhooks. Webhook responses now distinguish authentication, payload, processing, and missing-data errors.
Migration and legacy backfill
apps/website/app/lib/migrations.server.ts, apps/website/app/routes/health.tsx
The shared migration runner applies Drizzle migrations and fills missing purchase identities from active and legacy catalogs.
Pricing policy publication
apps/website/app/content/blog/ryot-pricing-is-changing/index.mdx, apps/website/app/routes/pricing-promise.tsx, apps/website/app/root.tsx, apps/website/react-router.config.ts, apps/website/app/routes/terms.tsx
The pricing announcement, pricing-promise page, footer navigation, and terms contact section describe and expose the updated pricing policy.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: versioned pricing catalogs and preservation of grandfathered purchase pricing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch update-prices

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9c37d60 and 76809f4.

📒 Files selected for processing (19)
  • apps/website/app/content/blog/ryot-pricing-is-changing/index.mdx
  • apps/website/app/drizzle/migrations/0008_yielding_carnage.sql
  • apps/website/app/drizzle/migrations/meta/0008_snapshot.json
  • apps/website/app/drizzle/migrations/meta/_journal.json
  • apps/website/app/drizzle/schema.server.ts
  • apps/website/app/lib/components/Pricing.tsx
  • apps/website/app/lib/config.server.ts
  • apps/website/app/lib/migrations.server.ts
  • apps/website/app/lib/payment-catalog.ts
  • apps/website/app/lib/pricing-config.ts
  • apps/website/app/lib/provisioning.server.ts
  • apps/website/app/lib/utilities.server.ts
  • apps/website/app/root.tsx
  • apps/website/app/routes/health.tsx
  • apps/website/app/routes/me.tsx
  • apps/website/app/routes/paddle-webhook.tsx
  • apps/website/app/routes/polar-webhook.tsx
  • apps/website/app/routes/pricing-promise.tsx
  • apps/website/react-router.config.ts
💤 Files with no reviewable changes (1)
  • apps/website/app/lib/pricing-config.ts

Comment thread apps/website/app/content/blog/ryot-pricing-is-changing/index.mdx Outdated
Comment thread apps/website/app/drizzle/migrations/0008_yielding_carnage.sql
Comment thread apps/website/app/drizzle/migrations/0008_yielding_carnage.sql
Comment thread apps/website/app/drizzle/schema.server.ts Outdated
Comment thread apps/website/app/lib/payment-catalog.ts Outdated
Comment thread apps/website/app/lib/payment-catalog.ts Outdated
Comment thread apps/website/app/lib/utilities.server.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 win

Reject orders without a Polar productPriceId.

getProductAndPlanTypeByPolarIds matches by productId alone when priceId is absent, and handlePurchaseOrRenewal stores providerPriceId as undefined. Add a required productPriceId check 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

📥 Commits

Reviewing files that changed from the base of the PR and between 76809f4 and ed7af5c.

📒 Files selected for processing (5)
  • apps/website/app/content/blog/ryot-pricing-is-changing/index.mdx
  • apps/website/app/drizzle/schema.server.ts
  • apps/website/app/lib/payment-catalog.ts
  • apps/website/app/routes/paddle-webhook.tsx
  • apps/website/app/routes/polar-webhook.tsx

Comment thread apps/website/app/drizzle/schema.server.ts
Comment thread apps/website/app/lib/payment-catalog.ts
Comment thread apps/website/app/routes/paddle-webhook.tsx
Comment thread apps/website/app/routes/paddle-webhook.tsx
@IgnisDa
IgnisDa merged commit 6c4d778 into main Aug 9, 2026
14 checks passed
@IgnisDa
IgnisDa deleted the update-prices branch August 9, 2026 12:24
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.

1 participant