Bases de datos para SaaS: Postgres + Drizzle en términos para principiantes
Guía para principiantes sobre PostgreSQL y Drizzle ORM en SaaS: tablas, filas y relaciones, por qué Postgres, esquema tipado en TypeScript, consultas, índices, seguridad de datos, migraciones y diferencias entre local y producción.
Bases de datos para SaaS: Postgres + Drizzle en términos para principiantes
Al construir una app SaaS, necesitas un lugar donde guardar datos: usuarios, pedidos, posts, etc. Ahí entra la base de datos. En este artículo desmitificamos las bases de datos usando PostgreSQL (Postgres) como elección por defecto y mostramos cómo usar el ORM Drizzle en TypeScript para interactuar con Postgres de forma amigable para principiantes. Veremos qué son tablas y filas (sin jerga), por qué Postgres es una gran opción, cómo Drizzle te permite definir y consultar el esquema con facilidad, y lo básico sobre consultas, índices y cuidado de tus datos. También explicamos la diferencia entre usar una base de datos en desarrollo local y en producción, y resolvemos dudas típicas como “¿necesito SQL?” y “¿qué pasa si cambio una columna?”. Al final deberías entender el papel de la base de datos en tu SaaS y sentirte cómodo ejecutando migraciones para evolucionar tu esquema. ¡Vamos!
Tablas, filas y relaciones (sin jerga)
Piensa en una tabla de base de datos como en una hoja de cálculo. Tiene columnas (campos) y filas (entradas):
- Tabla: colección de datos organizada en filas y columnas —por ejemplo, una tabla
users
. - Fila: una entrada de la tabla. En
users
, una fila representa una cuenta de usuario (los datos de una persona). - Columna: una pieza de información por fila, con nombre y tipo. En
users
podrías teneremail
,password
,created_at
, etc. Cada fila tendrá valores para esas columnas. - Clave primaria (Primary Key): columna(s) que identifican de forma única cada fila (el “ID” único del registro).
- Relación: conexión entre tablas. Por ejemplo, si tienes
sessions
para sesiones de login, podría teneruser_id
que referencia una fila enusers
. Así sabes a qué usuario pertenece la sesión. Esta vinculación de filas es la esencia de las bases de datos relacionales.
En español llano: una tabla es como Excel; cada fila es un registro (un usuario, un pedido…), cada columna es una propiedad (email, importe, fecha…), y las tablas se enlazan mediante IDs (un pedido enlaza al usuario que lo hizo).
Por qué PostgreSQL es un gran valor por defecto
Al elegir base de datos para un SaaS, PostgreSQL suele recomendarse como un gran valor por defecto [1]:
- Fiabilidad probada: Postgres lleva décadas y es sinónimo de robustez. Cumple ACID íntegramente (maneja transacciones de forma segura: no “pierde” dinero a mitad de una operación).
- Propósito general y potente: no es nicho. Sirve para cuentas de usuario, posts, transacciones financieras… Además tiene funciones avanzadas (columnas JSON, búsqueda full‑text, GIS…) si las necesitas luego.
- Escala bien: puedes empezar pequeño y crecer. Muchas startups empiezan con una instancia y escalan a millones de usuarios con buen tuning y hardware [1].
- Gran ecosistema y soporte: por su popularidad hay múltiples opciones gestionadas (incluso tiers gratis): Heroku Postgres, Neon, Supabase, Railway, AWS RDS, etc. [2] y una comunidad enorme.
- Open source y coste efectivo: es libre y sin licencias. Incluso los servicios gestionados suelen ser asequibles.
- Compatibilidad: muchas bases (Redshift, CockroachDB, TimescaleDB) hablan “dialecto Postgres” o se basan en él [2].
En resumen, Postgres es una base confiable y flexible. “Postgres es un gran default” por una razón: cubre muchos casos y tiene un historial de éxito [1].
Introducción a Drizzle ORM: esquema tipado en TypeScript
Ya tenemos base de datos; ¿cómo hablamos con ella desde el código? En lugar de escribir SQL crudo a mano, usamos un ORM (Object‑Relational Mapper). Drizzle es un ORM moderno para TypeScript que ofrece un enfoque tipado para bases SQL como Postgres. Define el esquema en TypeScript: describes tablas y columnas en código y Drizzle lo usa como fuente de verdad para consultas y migraciones [3]:
- Fuente única de verdad: el esquema en un solo sitio (código). Drizzle valida consultas y genera migraciones a partir de él.
- Tipado: al estar en TS, obtienes autocompletado y chequeo de tipos. Si consultas una columna inexistente, no compila.
- Sin “cadenas mágicas”: en vez de SQL como string, usas funciones/constantes verificables.
Ejemplo de tabla sessions
para sesiones de login:
import { pgTable, varchar, text, timestamp, index, uniqueIndex } from "drizzle-orm/pg-core";
export const sessions = pgTable(
"sessions",
{
id: varchar({ length: 255 }).primaryKey(), // Clave primaria (ID) tipo string
user_id: varchar({ length: 255 }).notNull(), // ID del usuario dueño de la sesión (obligatorio)
token: varchar({ length: 512 }).notNull(), // Token único de la sesión (obligatorio)
expires_at: timestamp({ withTimezone: true }).notNull(), // Cuándo expira la sesión
ip_address: varchar({ length: 255 }), // (Opcional) IP de la sesión
user_agent: text(), // (Opcional) User‑Agent del navegador/dispositivo
created_at: timestamp({ withTimezone: true }).notNull().defaultNow(),
updated_at: timestamp({ withTimezone: true }).notNull().defaultNow(),
},
(table) => [
// Índices y restricciones:
uniqueIndex("sessions_token_unique_idx").on(table.token), // Garantiza tokens únicos
index("sessions_user_id_idx").on(table.user_id), // Búsquedas rápidas por user_id
]
);
En llano:
pgTable("sessions", {...}, (table) => [...])
define la tablasessions
.- Columnas:
id
es la PK (varchar(255)
).user_id
es string obligatorio y relaciona conusers.id
.token
string obligatorio, que queremos único.expires_at
timestamp con zona horaria obligatorio.ip_address
yuser_agent
son opcionales.created_at
/updated_at
llevandefaultNow()
.
- Tercer argumento: índices/restricciones (único por token; índice por
user_id
para rendimiento).
Definir el esquema en código lo hace visible y versionable; Drizzle lo usa para migraciones.
¿Por qué Drizzle en Next.js/TS? Porque abraza TS: tipos generados, chequeo en compilación y un motor liviano que traduce a SQL de forma transparente (ideal incluso para entornos serverless).
Consultar datos con Drizzle (más índices y seguridad de datos)
Definir tablas es la mitad; también hay que consultar y modificar. Drizzle ofrece una API fluida para select/insert/update/delete
de forma tipada. Ejemplo: obtener créditos (transacciones) de un usuario y ordenarlos por fecha:
SELECT *
FROM credits
WHERE user_uuid = 'some-user-id'
ORDER BY created_at DESC;
En Drizzle:
import { db } from "@/db"; // instancia de Drizzle
import { credits as creditsTable } from "@/db/schema"; // esquema de la tabla credits
const userId = "some-user-uuid";
const userCredits = await db.select().from(creditsTable)
.where(creditsTable.user_uuid.eq(userId))
.orderBy(creditsTable.created_at.desc());
Notas:
creditsTable.user_uuid.eq(userId)
evita inyecciones (consulta parametrizada) y errores de tipeo.orderBy(...desc())
ordena descendente.- El resultado es un array con el tipo correcto en TS según el esquema.
Índices, restricciones y seguridad:
- Índices para rendimiento: añade índices a columnas por las que filtras o unes (foreign keys como
user_id
). Sin índice, las búsquedas se degradan al crecer la tabla. - Restricciones
UNIQUE
: integridad de datos (email único, token único, etc.). La BD rechaza duplicados. NOT NULL
: columnas obligatorias para evitar registros incompletos.- Tipos de datos: la BD hace cumplir tipos (no puedes guardar “Mañana” en un
timestamp
). TS te guía en inserts/updates. - Transacciones: para operaciones que deben “todo o nada” (mención breve).
Base de datos de desarrollo local vs de producción
Trabajar con una BD local (tu portátil) difiere de gestionarla en producción (app viva con usuarios reales). Distingue y trata cada una correctamente:
BD de desarrollo (local):
- Suele ejecutarse en tu máquina o Docker. Solo para construir/probar.
- Tienes libertad para experimentar: datos de prueba, resets, operaciones destructivas sin consecuencias graves.
- Conexión en
.env
(p. ej.,DATABASE_URL=postgres://postgres:password@localhost:5432/myapp_dev
). - En equipo: cada dev puede tener su BD local. No contiene datos reales.
- Rendimiento: una BD local es pequeña; una consulta instantánea en dev podría ser lenta en prod —usa índices y buenas prácticas desde temprano.
BD de producción:
- Es la real: guarda datos de usuarios. Trátala con máximo cuidado: no “experimentes” ahí.
- Suele ser un servicio gestionado (Heroku, AWS, GCP, Azure, Supabase, Neon…) con backups, updates y escalado.
- Usa otra cadena de conexión (env vars seguras en el servidor). Nunca reutilices la local.
- Migraciones en prod: prueba antes en dev y aplica durante el deploy cuando el código ya soporta los cambios [5].
- Volumen y monitorización: vigila rendimiento (consultas lentas, uso de índices). Activa logs/insights del proveedor.
- Backups y seguridad: habilita copias automáticas; restringe el acceso de red; contraseñas fuertes y parches al día.
En corto: una BD para dev (segura para iterar) y otra para prod (sagrada: protégela, haz backups y aplica cambios con cabeza). Nunca apuntes tu app local a la BD de producción.
Preguntas comunes de principiantes
P: ¿Necesito aprender SQL para usar Postgres y Drizzle?
R: No de inmediato, pero ayuda mucho a largo plazo. Un ORM como Drizzle te permite empezar sin escribir SQL crudo para cada operación. Aun así, entender fundamentos relacionales y algo de SQL te hará mejor desarrollador y te ayudará a optimizar o depurar [4]:
- Modelado de datos: diseño de tablas y relaciones.
- Consultas simples:
SELECT
,INSERT
,UPDATE
,DELETE
,WHERE
. - Joins: combinar datos relacionados.
- Agregaciones: contar, sumar, etc.
Piensa en el ORM como “ruedas auxiliares”: puedes avanzar sin dominar SQL, pero conocerlo te prepara para cualquier terreno.
P: ¿Qué pasa si cambio una columna o tabla más adelante? (¿Por qué importan las migraciones?)
R: El cambio es constante. Si modificas el esquema en el código de Drizzle, tu app esperará esa estructura; la BD real no cambiará hasta aplicar una migración. Sin migraciones, el código y la BD divergen y habrá errores.
Migraciones con Drizzle Kit [5]:
- Editas el esquema en TypeScript.
- Generas la migración (
drizzle-kit generate
), que compara con el snapshot anterior y crea SQL de cambios. - Revisa diferencias (renombres vs columnas nuevas) y confirma cuando proceda.
- Aplicas la migración (
drizzle-kit migrate
) sobre la BD. - Se actualiza el snapshot y comiteas el archivo de migración.
Ejemplo práctico: cambiar credits
de INT
a BIGINT
. Cambiar solo el esquema no modifica la columna existente; la migración generará el ALTER TABLE ...
necesario. En cambios incompatibles quizá necesites pasos manuales. En prod, respalda antes, aplica durante el deploy y, si es posible, escribe migraciones reversibles.
Siguientes pasos: pruébalo con migraciones de Drizzle (CTA)
Ahora que entiendes Postgres y Drizzle, toca practicar. En el contexto de tu proyecto (plantilla o starter), probablemente ya haya esquema y scripts listos. Sigue el README para generar y aplicar migraciones en tu BD local, por ejemplo:
npm run db:generate # genera una migración según diferencias de esquema
npm run db:migrate # aplica la migración a la base de datos
Abre el archivo generado (normalmente en drizzle/
o migrations/
) y revisa el SQL. Después, inserta datos de prueba con Drizzle, consúltalos y, si te animas, añade una nueva columna al esquema y repite la generación/aplicación. Así interiorizas el ciclo de migraciones.
¡Felicidades! Diste un gran paso en bases de datos para SaaS: fundamentos relacionales (tablas, filas y relaciones), por qué confiar en Postgres, cómo aprovechar Drizzle y la importancia de las migraciones. Con esto, estás preparado para construir sobre una base sólida de datos.
Referencias
Frontend vs Backend vs Full‑Stack para principiantes en SaaS
Guía para principiantes para entender frontend, backend y full‑stack en el contexto de un SaaS, con una analogía fácil de entender y ejemplos de una pila web moderna.
Guía de presupuesto SaaS — Costes, hosting y lo importante
Guía práctica sobre los costes reales de iniciar un SaaS: hosting, base de datos, pagos, email y en qué se gasta realmente. Empieza gratis, valida pronto y paga solo cuando creces.