Saltar al contenido
Todos los artículos

Migrar un SaaS existente a Supastack

Un manual por etapas para migrar un SaaS en funcionamiento — auth, tenants, facturación, jobs — sobre la base de supastack, sin reescritura ni fin de semana de caída.

Noah Zixin Zhang4 min de lectura

¿Cómo migro un SaaS existente a supastack?

Depende de tu base de código, pero la secuencia no cambia: migra por etapas, nunca en una sola reescritura. Haz inventario del repo separando el código de producto (rutas, esquema, lógica de dominio — sigue siendo tuyo) de la infraestructura (auth, tenants, facturación, jobs, email — la reemplaza el kit). Adopta los módulos en orden de dependencia: estructura, auth, workspaces, facturación y después el trabajo en segundo plano. Mueve los datos con un script reejecutable, conserva los IDs de cliente de Stripe y haz el corte cuando pase la aceptación. La mayoría de las apps web tardan días de tardes, no semanas.

Haz inventario antes de tocar el código

Toda migración que sale mal empezó de la misma forma: alguien abrió el repo del kit y empezó a borrar archivos. El kit es una base, no un tema — reemplaza tu infraestructura y no tiene opinión sobre tu producto. Antes de mover nada, divide honestamente tu repositorio en dos montones.

El código de producto es todo lo que un cliente podría describirte de vuelta: tus rutas, tu esquema, tu lógica de dominio, tus textos, tu página de precios. La infraestructura es todo lo que nunca ven: sesiones, comprobaciones de tenant, webhooks de pago, jobs en cola, correo transaccional. El primer montón migra intacto. El segundo se reemplaza — que es exactamente la idea.

  • Enumera cada grupo de rutas y etiquétalo como producto o fontanería — sé implacable; una página de ajustes que solo cambia una URL de webhook es fontanería.
  • Enumera tus variables de entorno y marca cuáles configuran infraestructura (secretos de auth, claves de proveedor) frente a comportamiento del producto.
  • Enumera tus tablas y marca cuáles serán propiedad del kit (users, sessions, workspaces, estado de facturación) frente a cuáles son tuyas.
  • Anota el único recorrido de usuario que te paga — ese flujo es la prueba de aceptación de cada etapa de abajo.

Asigna cada unión a un módulo del kit

La infraestructura del kit es un conjunto de módulos con contratos entre ellos — tu trabajo es apuntar cada pieza de tu vieja fontanería al módulo que ahora la posee. La mayor parte del mapeo es uno a uno; los puntos donde no lo es son donde tomas decisiones de verdad.

Dos decisiones de mapeo merecen reflexión real en lugar de un porteo mecánico. Si vienes de Prisma, el kit estandariza sobre Drizzle — puedes conservar Prisma para las tablas de producto durante la transición, pero planea converger; dos ORM en una sola base de código son un impuesto de mantenimiento que se acumula. La comparación Prisma vs Drizzle expone qué cambia.

Y si vienes de un proveedor de auth gestionada, resiste el impulso de replicar cada función del proveedor antes de cambiar. La auth gestionada empaqueta en silencio cosas que tu producto quizá no usa — listas de sesiones, UI de organizaciones, sincronización por webhook. Mapea lo que tu producto realmente llama; la comparación de auth cubre las diferencias de capacidad que importan.

Tu fontanería actualMódulo supastackQué se traslada
Sesiones de NextAuth.js / Auth.jsAutenticación (Better Auth)Users y cuentas migran; las sesiones se reemiten en el corte
Clerk o un proveedor de auth gestionadaAutenticación (Better Auth)Los users migran por correo; las sesiones de terceros se abandonan, no se reproducen
Tablas de org/equipo propiasEspacios de trabajoTus filas de org se convierten en filas de workspace + membresía
Webhooks de Stripe hechos a manoFacturaciónLos customer ID de Stripe sobreviven; el estado de suscripción se vuelve a derivar
Jobs de CRON + setTimeoutTrabajos (Trigger.dev)El trabajo programado pasa detrás del contrato JobRunner compartido
Nodemailer / correo ad hocCorreo electrónicoLas plantillas se trasladan; el envío pasa por un solo adaptador de proveedor

Adopta los módulos en orden de dependencia

El orden de migración no es una preferencia — es un grafo de dependencias. Cada etapa de abajo es lanzable por separado, y cada una produce un sistema funcional que puedes verificar antes de que empiece la siguiente. La tentación es hacerlo todo en un fin de semana y depurar la intersección; resístete — una migración por etapas significa que cada bug que encuentres pertenece exactamente a un módulo, un superpoder de depuración que no apreciarás hasta que lo omitas.

  • Etapa 1 — esqueleto: monta el kit, configura el env, coloca tus rutas de producto en la estructura del proyecto. Nada está conectado aún; la app arranca y tus páginas se renderizan.
  • Etapa 2 — auth: mueve el inicio de sesión a Better Auth, migra las tablas de user/account, verifica inicio + cierre de sesión de principio a fin. Cada etapa posterior depende de que exista una sesión.
  • Etapa 3 — tenancy: adopta el modelo de workspace, rellena workspaceId en tus tablas de producto, impón el scoping en lecturas y escrituras. Aquí es donde el post sobre fronteras de workspace se paga solo.
  • Etapa 4 — facturación: apunta el checkout y los webhooks al módulo de facturación, remapea las suscripciones existentes, verifica las comprobaciones de derechos contra datos reales de Stripe en modo test.
  • Etapa 5 — trabajo en segundo plano y correo: porta los jobs al contrato JobRunner y mueve el correo transaccional al adaptador de email. Van al final porque dependen de que ya existan users y workspaces.

Mueve los datos honestamente

La migración de datos es donde las migraciones se ganan su reputación. Las reglas que la mantienen aburrida: escribe un script que puedas ejecutar dos veces, nunca mutes la fuente antes del corte y verifica los conteos en lugar de confiar en el script.

Users y cuentas aterrizan en el esquema de Better Auth — el correo es la clave de unión, y las cuentas con contraseña exigen una decisión: o tu formato de hash actual es verificable por Better Auth, o lanzas un flujo de restablecimiento de contraseña en el corte y lo tratas como un forcing function. Las cuentas OAuth de terceros son el caso fácil — el proveedor las reemite en el siguiente inicio de sesión.

Para la facturación, el camino honesto es la re-derivación en lugar de copiar filas: conserva el customer ID de Stripe, deja que la gestión de webhooks del kit reconstruya el estado de derechos desde la verdad del proveedor y compara lo que calcula con tus registros de suscripción existentes. El ledger idempotente de webhooks del kit existe precisamente para que los eventos históricos reproducidos no provisionen nada dos veces.

// migration scripts are boring by design: upsert-shaped,
// re-runnable, and they verify counts instead of trusting themselves
for (const legacyUser of legacyUsers) {
  await upsertAuthUser({
    email: legacyUser.email.toLowerCase(),
    name: legacyUser.displayName,
    externalId: legacyUser.id, // preserved for one-shot joins
  });
}
const migrated = await countAuthUsers();
if (migrated !== legacyUsers.length) {
  throw new Error(`count mismatch: ${migrated} of ${legacyUsers.length}`);
}

Lo que sigue siendo tuyo

Vale la pena decirlo claramente, porque es la parte que la gente entiende mal en ambas direcciones: tus rutas de producto, tu esquema, tu lógica de dominio y tu diseño no cambian. No estás reconstruyendo tu app dentro del kit — estás apoyando tu app encima.

El error inverso es mantener viva la vieja fontanería «temporalmente» junto al nuevo módulo. Una segunda comprobación de sesión, un segundo handler de webhooks, una segunda tabla de derechos — todo sistema paralelo «temporal» es permanente hasta que alguien lo borra, y cada uno es un incidente futuro. Reemplaza, verifica, borra. Si una etapa aún no puede borrar el camino viejo, no está terminada.

La checklist del corte

El corte es una checklist, no un evento. Ejecútala al menos una vez en un entorno de staging con datos de forma producción antes de la fecha real.

  • Vuelve a ejecutar el script de datos contra un snapshot de producción fresco; los conteos y las comprobaciones puntuales pasan.
  • Inicia sesión con cada proveedor de auth que tu producto soporta — incluidos los que habías olvidado que existían.
  • Compra tu propio producto en modo test de Stripe y sigue el webhook hasta el derecho de acceso.
  • Envía cada correo transaccional una vez; comprueba que los enlaces aterrizan en el nuevo despliegue.
  • Verifica el recorrido de pago de principio a fin, luego cambia DNS/env y observa la primera sesión real.
  • Mantén el viejo despliegue en marcha una semana — el rollback es un cambio de DNS, no una migración.

Preguntas frecuentes sobre migrar a un starter kit

El mismo puñado de preguntas aparece cada vez que alguien se sienta a hacerlo.

  • ¿Cuánto tarda realmente una migración? — Para una sola app web con auth estándar + Stripe: días de tardes, no semanas. El B2B multi-tenant con facturación propia añade tiempo en las etapas tres y cuatro — es el trabajo de datos lo que se estira, nunca el traslado de archivos.
  • ¿Debería migrar o empezar de cero y portar funciones? — Si tu código de producto es más pequeño que tu deuda de infraestructura, empezar de cero sobre el kit y portar las rutas de producto suele ser más rápido. Si tu esquema y tu lógica de dominio son la parte difícil, migra en el sitio.
  • ¿Puedo conservar mi ORM / mi librería de auth durante la transición? — Por un tiempo, sí — una frontera de módulo cada vez. Para siempre, no: dos de cualquier cosa en la capa de infraestructura es un impuesto que se acumula.
  • ¿Y mis webhooks de Stripe ya en vuelo? — Apunta el endpoint de webhook existente al handler del kit una vez verificada la etapa cuatro; el ledger idempotente hace que los eventos reproducidos o duplicados sean seguros en lugar de peligrosos.
  • ¿Tengo que adoptar todos los módulos? — No. Móvil, escritorio, jobs e IA son superficies para adoptar o ignorar — la licencia es la base, no un mandato de lanzar una app por plataforma.