Desarrollo de aplicaciones SaaS
Desarrollo de SaaS con Next.js
Productos SaaS multi-tenant sobre App Router - tenencia y autorización decididas antes de la primera pantalla, facturación que aguanta una auditoría y un panel que se queda en el servidor.
Un producto SaaS no es una web con login. Tres decisiones de la primera quincena -cómo funciona la tenencia, dónde vive la autorización y cómo llega el estado de facturación a la aplicación- determinan lo que cuestan los dos años siguientes, y las tres son caras de revertir en cuanto hay datos en producción.
Nosotros construimos estos productos, y empezamos por esas tres.
Tenencia, decidida antes de la primera pantalla
Hay tres modelos y el correcto depende de tus clientes, no de la moda.
| Modelo | Aislamiento | Cuándo es el correcto |
|---|---|---|
| Tablas compartidas, columna de tenant | Impuesto en el código | La mayoría de productos. El más barato de operar y migrar. |
| Esquema por tenant | Impuesto por la base de datos | Clientes regulados, export o restore por tenant. |
| Base de datos por tenant | Total | Pocos clientes, grandes, aislados por contrato. |
El modelo compartido es el correcto casi siempre, y su único riesgo real es una consulta que olvida el filtro de tenant. Eso no es un problema de disciplina que se arregle en revisión de código: se arregla con seguridad a nivel de fila en la base de datos, de modo que un filtro ausente devuelve nada en lugar de devolver los datos de otro.
Autorización en los datos, no en la ruta
El middleware que decide quién puede ver una página es una puerta, y las puertas sirven. Autorización no son. Las Server Actions y los Route Handlers se pueden llamar directamente, así que una comprobación que solo vive en el middleware es una comprobación que el endpoint no tiene.
Cada consulta lleva el tenant y el actor. Cada mutación vuelve a derivar el permiso de la sesión en vez de fiarse de un argumento. El razonamiento es el mismo de Server Actions y los tres errores de siempre, y en un producto multi-tenant el fallo no es un bug: es un cliente leyendo los datos de otro.
Facturación que aguanta una auditoría
Stripe es lo habitual y la integración no es la parte difícil. La parte difícil es que tu aplicación y Stripe guardan el mismo hecho -a qué tiene derecho este cliente- y van a discrepar.
Tratamos a Stripe como fuente de verdad y a la copia de la aplicación como una caché reconstruida desde webhooks, con el handler idempotente y con verificación de firma. Los derechos se leen de tu propia base de datos en cada petición, porque llamar a Stripe en el camino de la petición hace que la disponibilidad de tu producto dependa de la suya.
Prorrateos, pruebas, cambios de asientos y pagos fallidos son cada uno un estado que la interfaz tiene que expresar. Qué puede seguir haciendo una cuenta con impago es una decisión de producto, y la preguntamos en vez de inventarla.
El problema del panel
Los paneles SaaS son donde las aplicaciones de App Router se vuelven lentas. El
instinto es un use client arriba del todo porque hay gráficos y filtros, y
entonces el árbol entero del panel se va al navegador.
Las tablas, las cabeceras, la navegación y la obtención de datos se quedan en el servidor. El gráfico, el filtro y el selector de fechas son islas de cliente. El estado en la URL lleva los filtros, lo que hace una vista filtrada compartible y el botón atrás correcto. Y ninguna página espera a su widget más lento: cada uno llega por streaming detrás de su propia frontera, que es el sentido de loading.tsx no es un spinner.
Lo que no hacemos
No partimos de un boilerplate de SaaS. Codifican decisiones de tenencia, auth y facturación tomadas para otro producto, y deshacerlas después cuesta más que las dos semanas que ahorraron.
Tampoco construimos el panel de administración primero. Es la parte que más disfrutan especificando los fundadores y la parte que los clientes no ven nunca.
