docs: add merchant migrations design doc#12502
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Preview Environment |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 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. |
There was a problem hiding this comment.
Do you imagine this is something we do over API then?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
What's the rationale behind this?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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()] |
There was a problem hiding this comment.
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.)
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? |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Maybe a bit too involved, but I think we could also perform an OAuth connection: https://docs.stripe.com/stripe-apps/api-authentication/oauth
There was a problem hiding this comment.
let me explore this further 👀
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
docs.jsonvalidated as JSON)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.
handbook/engineering/design-documents/merchant-migrations.mdx.handbook/docs.jsonnavigation.Written for commit 9b92634. Summary will update on new commits.