Guía paso a paso para migrar un SaaS Next.js de Stripe a Polar: abstracción de pagos, webhooks, sandbox y deploy en sombra.
Si tienes un SaaS indie vendiendo a Europa, probablemente has notado el tirón de migrar de Stripe a Polar. La razón principal: Polar es Merchant of Record y se ocupa del IVA por ti.
La migración técnica no es tan dolorosa como suena. Si tu integración de Stripe está bien modularizada, son ~2 horas de trabajo. Esta guía explica cómo lo haríamos en una app Next.js existente.
Por eso la migración funciona mejor con un SaaS joven (< 100 suscripciones) o con un cambio gradual: clientes nuevos en Polar, antiguos siguen en Stripe hasta que churneen.
bun add @polar-sh/sdkMantén stripe por ahora; lo eliminas al final.
Mantén las de Stripe y añade las de Polar:
POLAR_ACCESS_TOKEN=polar_oat_...
POLAR_WEBHOOK_SECRET=whsec_...
POLAR_SERVER=productionReplica los productos que tienes en Stripe. Ojo con:
Si ya tienes una capa de abstracción tipo PaymentService o lib/payments/, esto lo tienes hecho.
Si no, ahora es el momento.
src/lib/payments/types.ts:
export interface PaymentProvider {
createCheckout(params: CheckoutParams): Promise<{ url: string }>;
cancelSubscription(subscriptionId: string): Promise<void>;
verifyWebhook(body: string, signature: string): Promise<WebhookEvent>;
}Implementa StripeProvider y PolarProvider. Cambias entre ellos con una env var:
const provider =
process.env.PAYMENT_PROVIDER === 'polar' ? new PolarProvider() : new StripeProvider();Stripe y Polar firman webhooks de forma distinta. Crea un endpoint nuevo, no modifiques el de Stripe:
src/app/api/webhooks/polar/route.ts:
import { polar } from '@/lib/polar/client';
import { db } from '@/lib/db/client';
export async function POST(req: Request) {
const body = await req.text();
const signature = req.headers.get('webhook-signature')!;
const event = polar.webhooks.verify({
body,
signature,
secret: process.env.POLAR_WEBHOOK_SECRET!,
});
// Reusa la lógica del webhook de Stripe, adaptada a eventos de Polar
switch (event.type) {
case 'subscription.created':
// crear en DB
break;
case 'subscription.canceled':
// marcar como canceled
break;
}
return new Response('OK');
}En tu DB, añade un campo paymentProvider a la tabla de suscripciones:
model Subscription {
id String @id
userId String
paymentProvider String // 'stripe' | 'polar'
externalId String // ID en Stripe o Polar
status String
...
}Cuando un usuario cancela y se vuelve a suscribir, su nueva suscripción va a Polar. Las antiguas se quedan en Stripe hasta que churneen.
Polar tiene sandbox separado. Antes de tocar producción:
POLAR_SERVER=sandbox4242 4242 4242 4242Cuando todo funciona en sandbox → cambia a production.
No anuncies la migración el día 1. Despliega Polar en producción con:
1. Webhook duplicado: si reproduces el mismo evento, asegúrate de que tu DB no crea
suscripciones duplicadas. Indexa por externalId.
2. IVA en pricing visible: si Stripe mostraba "10€ IVA incluido", en Polar el cliente verá "10€ + IVA" porque Polar lo calcula. O bajas el precio neto, o subes el visible. Comunica.
3. Reembolsos y disputes: la lógica es distinta. Lee la documentación de Polar antes del primer reembolso.
Migrar Stripe → Polar son ~2 horas de código si tienes la abstracción hecha, o ~1 día desde cero. La parte más larga es probar en sandbox y hacer el shadow deploy.
A cambio: te olvidas del IVA, de la liquidación OSS, del gestor discutiendo qué tipo de servicio es un SaaS. Si vendes a Europa, vale la pena.
Suscríbete para más tutoriales y tips sobre crear productos con IA
La integración real de Tailwind v4 + shadcn 2.3.0 en Next.js 16 App Router. OKLCH, dark mode sin FOUC, Geist con next/font y el bug silencioso de hsl(var()) que rompe tus estilos sin avisar.