Skip to content

docs: add merchant migrations design doc#12502

Open
psincraian wants to merge 2 commits into
mainfrom
psincraian/merchant-migrations-rfc
Open

docs: add merchant migrations design doc#12502
psincraian wants to merge 2 commits into
mainfrom
psincraian/merchant-migrations-rfc

Conversation

@psincraian

@psincraian psincraian commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an engineering design document (RFC) for migrating merchants to Polar from Stripe, Lemon Squeezy, and Paddle, and registers it in the handbook navigation.

The design it's generic, but for now it's more focused from Stripe to Polar.

Checklist

  • This PR addresses a single concern (one feature, one doc)
  • The diff is reasonably sized and easy to review
  • New functionality is covered by tests
  • Linting and type checking pass (docs-only; docs.json validated as JSON)
  • No unrelated changes or drive-by fixes are included

Summary by cubic

Adds a new engineering design doc for migrating merchants from Stripe, Lemon Squeezy, and Paddle to Polar, covering catalog/data import and payment-method transfer via Stripe PAN Copy or PAN Import. Includes per-subscription cutover, prechecks (clarifies single account currency requirement), a Stripe→Polar mapping, and a live-only rollout plan.

  • New Features
    • Added handbook/engineering/design-documents/merchant-migrations.mdx.
    • Registered the page in handbook/docs.json navigation.

Written for commit 9b92634. Summary will update on new commits.

Review in cubic

RFC for migrating merchants from Stripe, Lemon Squeezy, and Paddle to
Polar: catalog/data import plus payment-method migration via Stripe PAN
Copy and PAN Import. Covers the migration phases, per-subscription cutover,
prechecks, a Stripe to Polar mapping, and a live-only testing/rollout plan.

Registers the page in the handbook navigation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orbit Ready Ready Preview, Comment Jun 18, 2026 10:15am
polar Ready Ready Preview, Comment Jun 18, 2026 10:15am
polar-sandbox Ready Ready Preview, Comment Jun 18, 2026 10:15am
polar-test Ready Ready Preview, Comment Jun 18, 2026 10:15am

Request Review

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Preview Environment
URL: https://polar-preview-vm.taildbff7b.ts.net/pr-12502
API: https://polar-preview-vm.taildbff7b.ts.net/pr-12502/v1/
Logs: backend
SHA: 9b92634e88e63420a3180e3642ac3012b0c4d01d

@mintlify

mintlify Bot commented Jun 18, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Handbook 🟢 Ready View Preview Jun 18, 2026, 9:32 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

1. Check the subscription is still active on the current billing provider.
2. Confirm the renewal date is far enough out (24h safety window), so we don't clash with the old billing provider renewal.
3. Confirm the card is valid on Polar (with a SetupIntent).
4. Cancel the subscription on the old billing provider.

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.

Do you imagine this is something we do over API then?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think would be the best, we keep the burden of deciding and doing the migration of subscriptions.

In the future we can provide some api endpoints to the merchant to do that on their own and with their own provider if there is the need. But I'm worried about double charging customers and I don;t thik that's needed for now


Polar will bill the next billing cycle. If for some reason the payment fails on the next billing cycle, the subscription will enter the normal dunning state where we send an email to the customer.

We import each subscription as tax-inclusive by setting its own `tax_behavior = inclusive`, leaving the org's `default_tax_behavior` untouched - renewal billing reads the per-subscription value and only falls back to the org default when it's null.

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.

What's the rationale behind this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For example, I have my own subscription on pepy.tech where I still charge the old $5 dollars every month. I don't want this to be increased for the customers, so I take that cost on me instead of the customers for VAT.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would say it depends on the behavior of the source provider:

  • If tax is not applied, or already tax inclusive, then it makes sense to set tax inclusive
  • However, if exclusive tax is applied, then we should apply that setting as well — otherwise, the merchant will lose money and the final price for the customer will decrease

Comment thread handbook/engineering/design-documents/merchant-migrations.mdx Outdated

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 2 files

Confidence score: 4/5

  • In handbook/engineering/design-documents/merchant-migrations.mdx, the example currently reads the entire source dataset into memory instead of using incremental extraction/streaming, which could lead teams to implement migration jobs that run out of memory on large merchants — update the example to a batched or streaming pattern before merging.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="handbook/engineering/design-documents/merchant-migrations.mdx">

<violation number="1" location="handbook/engineering/design-documents/merchant-migrations.mdx:194">
P2: This example loads the full source dataset into memory, which is risky for large merchant migrations and conflicts with the incremental extraction design.

(Based on your team's feedback about streaming large batches.) [FEEDBACK_USED]</violation>
</file>

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Re-trigger cubic


```python
# 1. read + normalize from the source
records = [r async for r in StripeAdapter(api_key="rk_live_…").extract()]

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.

P2: This example loads the full source dataset into memory, which is risky for large merchant migrations and conflicts with the incremental extraction design.

(Based on your team's feedback about streaming large batches.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At handbook/engineering/design-documents/merchant-migrations.mdx, line 194:

<comment>This example loads the full source dataset into memory, which is risky for large merchant migrations and conflicts with the incremental extraction design.

(Based on your team's feedback about streaming large batches.) </comment>

<file context>
@@ -0,0 +1,246 @@
+
+```python
+# 1. read + normalize from the source
+records = [r async for r in StripeAdapter(api_key="rk_live_…").extract()]
+# records = [CanonicalProduct(...), CanonicalCustomer(...), CanonicalSubscription(...)]
+
</file context>

- Keep everything behind a feature flag, enabled only for those orgs during the testing phase and removed once we're confident.

## Open questions
1. Where and how should we store the API keys of Stripe, LS, Paddle? Do we have a convention to encrypt them?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We don't, but maybe a good occasion to start using AWS Secrets Manager or similar


### Reading the account

We can't reuse Polar's platform Stripe service (it's bound to the global platform key). Read the merchant account with a dedicated `StripeClient(api_key="rk_…")`. Required restricted-key read and write scopes: Customers, Payment Methods, Products, Prices, (read and write)Subscriptions, Coupons (and Invoices / Charges for the refund/dispute precheck). Mention those scopes in the merchant UI.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe a bit too involved, but I think we could also perform an OAuth connection: https://docs.stripe.com/stripe-apps/api-authentication/oauth

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

let me explore this further 👀

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.

3 participants