Migration Guide
Migration Guide
This guide covers how to migrate to QZPay from existing billing implementations or between payment providers.
Migrating from Direct Stripe SDK
If you’re currently using the Stripe SDK directly, QZPay provides a smooth migration path.
Before You Start
Step-by-Step Migration
-
Install QZPay packages
Terminal window pnpm add @qazuor/qzpay-core @qazuor/qzpay-stripe @qazuor/qzpay-drizzle -
Set up the database schema
Terminal window # Initialize QZPay schemapnpm qzpay init# Run migrationspnpm qzpay migrate -
Configure QZPay with your existing Stripe key
import { createQZPayBilling } from '@qazuor/qzpay-core';import { createQZPayStripeAdapter } from '@qazuor/qzpay-stripe';import { createQZPayDrizzleAdapter } from '@qazuor/qzpay-drizzle';const billing = createQZPayBilling({paymentAdapter: createQZPayStripeAdapter({secretKey: process.env.STRIPE_SECRET_KEY! // Same key you're already using}),storage: createQZPayDrizzleAdapter(db)}); -
Sync existing data from Stripe
Terminal window # Sync all datapnpm qzpay sync --provider stripe# Or sync specific resourcespnpm qzpay sync --provider stripe --resources customers,subscriptions -
Update your code incrementally
Replace Stripe SDK calls with QZPay calls one at a time:
// Before: Direct Stripeconst customer = await stripe.customers.create({email: 'user@example.com',metadata: { userId: '123' }});// After: QZPayconst customer = await billing.customers.create({email: 'user@example.com',metadata: { userId: '123' }}); -
Update webhook handlers
// Before: Direct Stripe webhookapp.post('/webhooks/stripe', async (req, res) => {const event = stripe.webhooks.constructEvent(req.body,req.headers['stripe-signature'],webhookSecret);switch (event.type) {case 'customer.subscription.updated':// Handle subscription updatebreak;}});// After: QZPay webhook with Hono integrationimport { createWebhookRouter } from '@qazuor/qzpay-hono';const webhookRouter = createWebhookRouter({billing,paymentAdapter: stripeAdapter,handlers: {'customer.subscription.updated': async (c, event) => {// Handle subscription update}}});app.route('/webhooks/stripe', webhookRouter);// Also use billing events for cross-cutting concernsbilling.on('subscription.updated', async (event) => {// Handle subscription update});
Mapping Stripe Concepts to QZPay
| Stripe Concept | QZPay Equivalent |
|---|---|
stripe.customers | billing.customers |
stripe.subscriptions | billing.subscriptions |
stripe.paymentIntents | billing.payments |
stripe.invoices | billing.invoices |
stripe.products | Part of Plans |
stripe.prices | billing.prices |
stripe.webhooks | createWebhookRouter() from @qazuor/qzpay-hono |
Migrating from MercadoPago
-
Install packages
Terminal window pnpm add @qazuor/qzpay-core @qazuor/qzpay-mercadopago @qazuor/qzpay-drizzle -
Configure and sync
import { createQZPayMercadoPagoAdapter } from '@qazuor/qzpay-mercadopago';const billing = createQZPayBilling({paymentAdapter: createQZPayMercadoPagoAdapter({accessToken: process.env.MP_ACCESS_TOKEN!}),storage: createQZPayDrizzleAdapter(db)});Terminal window pnpm qzpay sync --provider mercadopago -
Update code references
// Before: MercadoPago SDKconst preference = await mercadopago.preferences.create({items: [{ title: 'Pro Plan', unit_price: 99, quantity: 1 }]});// After: QZPay (using payment adapter)const checkout = await mpAdapter.checkout.create({customerId: providerCustomerId,mode: 'subscription',lineItems: [{ priceId: 'pro_monthly', quantity: 1 }],successUrl: 'https://example.com/success',cancelUrl: 'https://example.com/cancel'}, ['pro_monthly']); // Provider price IDs
Migrating Between Providers
One of QZPay’s key features is the ability to switch payment providers.
Stripe to MercadoPago (or vice versa)
-
Add the new provider
Terminal window pnpm add @qazuor/qzpay-mercadopago -
Configure multi-provider setup
import { createQZPayBilling } from '@qazuor/qzpay-core';import { createQZPayStripeAdapter } from '@qazuor/qzpay-stripe';import { createQZPayMercadoPagoAdapter } from '@qazuor/qzpay-mercadopago';// Keep existing provider for current customersconst stripeBilling = createQZPayBilling({paymentAdapter: createQZPayStripeAdapter({ secretKey: stripeKey }),storage});// New provider for new customersconst mpBilling = createQZPayBilling({paymentAdapter: createQZPayMercadoPagoAdapter({ accessToken: mpToken }),storage});// Or use region-based routingfunction getBilling(region: string) {if (region === 'LATAM') return mpBilling;return stripeBilling;} -
Migrate customers gradually
For each customer you want to migrate:
async function migrateCustomer(customerId: string) {// Get customer from old providerconst customer = await stripeBilling.customers.get(customerId);// Create in new providerconst newCustomer = await mpBilling.customers.create({email: customer.email,name: customer.name,metadata: {...customer.metadata,migratedFrom: 'stripe',originalId: customer.externalIds.stripe}});// Update local record to point to new providerawait storage.customers.update(customerId, {externalIds: {...customer.externalIds,mercadopago: newCustomer.externalIds.mercadopago}});} -
Handle active subscriptions
async function migrateSubscription(subscriptionId: string) {const subscription = await stripeBilling.subscriptions.get(subscriptionId);// Cancel at period endawait stripeBilling.subscriptions.cancel(subscriptionId, {atPeriodEnd: true});// Schedule new subscription creationawait scheduler.schedule({at: subscription.currentPeriodEnd,task: 'createNewSubscription',data: {customerId: subscription.customerId,planId: subscription.planId,provider: 'mercadopago'}});}
Database Migration
From Custom Schema to QZPay Schema
If you have existing billing data in a custom schema:
-
Export your existing data
// Export existing customersconst customers = await db.query('SELECT * FROM your_customers');// Transform to QZPay formatconst transformed = customers.map(c => ({email: c.email,name: c.full_name,metadata: {legacyId: c.id,// Map other fields}})); -
Initialize QZPay schema
Terminal window pnpm qzpay initpnpm qzpay migrate -
Import transformed data
for (const customer of transformed) {await billing.customers.create(customer);} -
Sync with provider
Terminal window # Ensure provider records matchpnpm qzpay sync --provider stripe --direction both
Maintaining Data Integrity
During migration, maintain referential integrity:
// Create mapping tableconst idMapping = new Map<string, string>();
// Import customers firstfor (const customer of legacyCustomers) { const newCustomer = await billing.customers.create({ email: customer.email, metadata: { legacyId: customer.id } }); idMapping.set(customer.id, newCustomer.id);}
// Then import subscriptions using mapped IDsfor (const subscription of legacySubscriptions) { const newCustomerId = idMapping.get(subscription.customer_id); await billing.subscriptions.create({ customerId: newCustomerId!, planId: mapPlanId(subscription.plan_id) });}Rollback Strategy
Always have a rollback plan:
// Before migration, create backupawait billing.export({ format: 'json', output: `./backup-${Date.now()}.json`});
// If migration fails, restoreawait billing.import({ file: './backup-timestamp.json', strategy: 'replace'});Post-Migration Checklist
After completing migration:
- Verify all customers are synced
- Verify all subscriptions are active
- Test webhook handling
- Test checkout flows
- Test subscription lifecycle (upgrade, downgrade, cancel)
- Update environment variables
- Update documentation
- Monitor for errors in first 24-48 hours