Empezamos con SQLite. Fue un Error Que Nos Costó Semanas Corregir.

Cuando empezamos a construir apps en iAmanos, usamos SQLite para las primeras aplicaciones. La razón era obvia: cero configuración. Sin servidor de base de datos, sin contenedores extra, sin cadenas de conexión. Un solo archivo .db y listo.

Funcionó perfecto… en desarrollo. Y funcionó aceptable para los primeros 10-20 usuarios. Pero cuando las apps empezaron a tener tráfico real, múltiples usuarios simultáneos, y necesidades de backup y monitoreo, SQLite se convirtió en un cuello de botella.

La migración a PostgreSQL + Prisma 7 fue una de las mejores decisiones técnicas que hemos tomado. Hoy, las 15+ apps principales corren en PostgreSQL con el nuevo adapter PrismaPg de Prisma 7, cada una en su propio contenedor Docker aislado.

Este artículo es la historia completa: por qué migramos, cómo lo hicimos, los gotchas que casi nos matan, y por qué nunca volvemos atrás.

Los 5 Problemas de SQLite en Producción Que Nos Obligaron a Migrar

1. Concurrencia: SQLITE_BUSY en el peor momento

SQLite usa un lock a nivel de archivo. Cuando un proceso está escribiendo, todos los demás procesos que quieran escribir tienen que esperar. Con Next.js y sus Server Components ejecutando queries en paralelo, los SQLITE_BUSY empezaron a aparecer cuando apenas teníamos 15-20 usuarios simultáneos.

El error típico: un usuario intenta guardar un formulario mientras otro está cargando un dashboard que dispara 5 queries. El formulario falla con un timeout. El usuario piensa que la app está rota.

2. Sin acceso remoto a la base de datos

SQLite es un archivo local. No puedes conectarte remotamente para debugging, no puedes usar herramientas como pgAdmin o DBeaver, no puedes hacer queries ad-hoc desde tu máquina de desarrollo. Para ver datos en producción tenías que SSH al servidor y usar el CLI de sqlite3.

3. Backups frágiles

Copiar el archivo .db mientras la app está escribiendo puede producir un backup corrupto. SQLite tiene .backup command, pero requiere parar las escrituras. En una app 24/7, eso no es aceptable.

4. Sin JSON columns nativos

SQLite tiene soporte limitado para JSON. PostgreSQL tiene jsonb nativo con indexación, queries dentro del JSON, y operators potentes. Para apps que almacenan datos semi-estructurados (configuraciones, metadatos, logs), esta diferencia es enorme.

5. Sin full-text search

PostgreSQL tiene full-text search nativo con tsvector, ranking por relevancia, y soporte para español. SQLite tiene FTS5 pero la integración con Prisma es complicada. Para apps con buscadores (como la búsqueda de razas en WouWou), PostgreSQL es infinitamente superior.

Por Qué PostgreSQL (Y No MySQL, MongoDB, o Supabase)

PostgreSQL vs MySQL

Ambos son excelentes bases de datos relacionales. Elegimos PostgreSQL por:

  • jsonb nativo: Columnas JSON con indexación y queries — MySQL lo tiene pero PostgreSQL es superior
  • Prisma support: Prisma tiene mejor soporte y más features para PostgreSQL
  • Extensiones: PostGIS para geolocalización, pg_trgm para búsqueda fuzzy
  • Comunidad: Más momentum en 2026, especialmente en el ecosistema Node.js/TypeScript

PostgreSQL vs MongoDB

MongoDB es bueno para datos no estructurados. Pero nuestras apps son CRUD con relaciones claras (usuarios → citas → mascotas → historial). Las relaciones son el corazón de nuestros modelos. PostgreSQL con Prisma nos da schemas tipados, migraciones declarativas, y relaciones first-class.

PostgreSQL vs Supabase

Supabase es PostgreSQL managed con extras (auth, storage, realtime). Es excelente, pero:

  • El free tier tiene límites que nuestras 15+ apps superarían rápido
  • El plan pro ($25/app/mes) nos costaría $375/mes solo en bases de datos
  • Con Docker en nuestro VPS, corremos 15 PostgreSQL por $0 adicional

Prisma 7 con PrismaPg: El Setup Que Usamos en Producción

Prisma 7 introdujo un cambio importante: el adapter pattern. En vez de que Prisma maneje la conexión directamente, usas un adapter que habla con tu driver de base de datos.

prisma.config.ts

// prisma.config.ts
import path from 'path';
import type { PrismaConfig } from 'prisma';

export default {
  earlyAccess: true,
  schema: path.join('prisma', 'schema.prisma'),
} satisfies PrismaConfig;

lib/prisma.ts

// lib/prisma.ts
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import pg from 'pg';

const connectionString = process.env.DATABASE_URL!;

const pool = new pg.Pool({ connectionString });
const adapter = new PrismaPg(pool);

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter });

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma;
}

Nota clave: en Prisma 7 con PrismaPg, la URL de conexión NO va en el schema.prisma. Va en el adapter que instancias en lib/prisma.ts. Si pones url en el schema, Prisma intenta usar su driver nativo en vez del adapter.

schema.prisma (sin url)

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  // NO url aquí — se maneja via PrismaPg adapter
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  role      String   @default("user")
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

El Gotcha Más Doloroso: Prisma CLI Falla en Docker Build

Este es el problema que más horas nos costó resolver y que casi ningún tutorial menciona:

Prisma 7 CLI intenta conectarse a la base de datos durante el build de Docker. Pero durante el build, el contenedor de PostgreSQL no está corriendo todavía. Resultado: el build falla.

# Esto FALLA en Docker build:
RUN npx prisma migrate deploy
# Error: Can't reach database server at `postgres-wouwou:5432`

Nuestra solución: crear tablas via psql

En vez de usar prisma migrate deploy en el Dockerfile, generamos el SQL de migración una vez localmente y lo aplicamos directamente con psql al levantar el contenedor de PostgreSQL.

# 1. Generar SQL localmente
npx prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script > init.sql

# 2. Copiar init.sql al directorio de Docker
# 3. En docker-compose.yml, montar como init script de PostgreSQL:
services:
  postgres-myapp:
    image: postgres:16-alpine
    volumes:
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
      - myapp-pgdata:/var/lib/postgresql/data

PostgreSQL ejecuta automáticamente los scripts en /docker-entrypoint-initdb.d/ cuando inicializa una base de datos nueva. Así las tablas se crean sin necesidad de Prisma CLI en el contenedor de la app.

Para migraciones subsecuentes

Cuando necesitas cambiar el schema (agregar columna, crear tabla, etc.):

  1. Modifica schema.prisma localmente
  2. Genera el diff: npx prisma migrate diff --from-url $OLD_DB --to-schema-datamodel prisma/schema.prisma --script > migration.sql
  3. Revisa el SQL generado
  4. Aplica en producción: docker exec postgres-myapp psql -U myuser -d mydb -f /path/migration.sql

Es más manual que prisma migrate deploy, pero funciona de forma confiable en Docker.

Cómo Migramos de SQLite a PostgreSQL: Paso a Paso

Para las apps que ya estaban en producción con SQLite, el proceso de migración fue:

Paso 1: Export de SQLite

# Exportar datos de SQLite como SQL
sqlite3 production.db .dump > sqlite_dump.sql

Paso 2: Limpiar el SQL

SQLite y PostgreSQL tienen diferencias de sintaxis. Necesitas ajustar:

  • AUTOINCREMENTSERIAL o GENERATED ALWAYS AS IDENTITY
  • INTEGER PRIMARY KEYSERIAL PRIMARY KEY o TEXT PRIMARY KEY (si usas CUIDs)
  • Booleanos: SQLite usa 0/1, PostgreSQL usa TRUE/FALSE
  • Timestamps: SQLite guarda como texto, PostgreSQL como TIMESTAMPTZ

Paso 3: Crear las tablas en PostgreSQL con Prisma

# Cambiar provider en schema.prisma de sqlite a postgresql
# Generar SQL de tablas
npx prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script > create_tables.sql

# Aplicar
psql -U myuser -d mydb -f create_tables.sql

Paso 4: Insertar datos

# Script Python que lee SQLite e inserta en PostgreSQL
import sqlite3
import psycopg2

sqlite_conn = sqlite3.connect('production.db')
pg_conn = psycopg2.connect('postgresql://user:pass@localhost/mydb')

# Para cada tabla, SELECT de SQLite e INSERT en PostgreSQL
for table in ['users', 'posts', 'comments']:
    rows = sqlite_conn.execute(f'SELECT * FROM {table}').fetchall()
    # Insert en PostgreSQL con psycopg2
    ...

Paso 5: Verificar integridad

# Comparar conteos
SELECT COUNT(*) FROM users; -- Debe coincidir en ambas DBs
SELECT COUNT(*) FROM posts;
# Verificar foreign keys
SELECT * FROM posts WHERE user_id NOT IN (SELECT id FROM users);

Tiempo total de migración por app: 2-4 horas dependiendo del volumen de datos y la complejidad del schema.

Performance: PostgreSQL vs SQLite en Nuestras Apps

Benchmarks reales de una de nuestras apps con ~1,000 records en la tabla principal:

  • Query simple (SELECT por ID): SQLite ~2ms, PostgreSQL ~3ms. SQLite gana marginalmente por ser local.
  • Query compleja (JOIN + filtros + ORDER BY + LIMIT): SQLite ~15ms, PostgreSQL ~8ms. PostgreSQL gana por optimizador superior.
  • Escritura concurrente (10 INSERTs simultáneos): SQLite ~500ms (serializados por lock), PostgreSQL ~20ms (paralelos). PostgreSQL aplasta.
  • Full-text search: SQLite FTS5 ~10ms, PostgreSQL tsvector ~4ms. PostgreSQL gana con mejor ranking.
  • JSON query: SQLite json_extract ~8ms, PostgreSQL jsonb ->> ~3ms. PostgreSQL gana por indexación nativa.

En lectura simple, SQLite es comparable o ligeramente más rápido (no hay red de por medio). En todo lo demás — especialmente concurrencia — PostgreSQL es objetivamente superior.

Cuándo SQLite Todavía Tiene Sentido

SQLite no es malo. Es una base de datos brillante para ciertos casos:

  • Prototipos y MVPs de 1 día: Cero configuración. npx prisma db push y tienes tablas.
  • Apps read-heavy con un solo proceso: Blogs, portafolios, landing pages con datos dinámicos.
  • Apps desktop/mobile: SQLite es el estándar para almacenamiento local.
  • Desarrollo local: Algunos devs prefieren SQLite en dev y PostgreSQL en producción. Funciona, pero nosotros preferimos usar PostgreSQL en ambos para evitar sorpresas.
  • Apps de bajo tráfico: Si tu app tiene <10 usuarios simultáneos, SQLite funciona bien.

Nuestra regla: si la app va a tener más de 1 usuario simultáneo en producción, usa PostgreSQL desde el día 1. El tiempo que ahorras con SQLite en el setup lo pierdes 10x cuando migras después.

En iAmanos todas las apps nuevas arrancan con PostgreSQL + Prisma 7 + Docker.

10 Tips de PostgreSQL en Producción Que Aprendimos por las Malas

1. Siempre usa volúmenes nombrados en Docker

Un docker compose down SIN volumen nombrado borra tu base de datos. Con volumen nombrado (myapp-pgdata:/var/lib/postgresql/data), los datos persisten aunque destruyas y recrees el contenedor.

2. Limita las conexiones por pool de Prisma

Prisma por default abre 5 conexiones por pool. Con 15 apps, son 75 conexiones simultáneas a PostgreSQL. Si tu VPS tiene poca RAM, reduce a 2-3 con connection_limit en el connection string: ?connection_limit=3.

3. Haz vacuum automático

PostgreSQL necesita VACUUM para reclamar espacio de filas eliminadas. El autovacuum está habilitado por default, pero para tablas con muchas actualizaciones (como logs de chat o sesiones), considera ajustar los parámetros o ejecutar vacuum manual periódicamente.

4. Usa EXPLAIN ANALYZE para queries lentas

Cuando una query toma más de 100ms, usa EXPLAIN ANALYZE para ver el plan de ejecución. Frecuentemente la solución es agregar un índice en la columna de WHERE o JOIN.

5. Nunca expongas PostgreSQL a la red pública

El contenedor de PostgreSQL debe estar SOLO en la red Docker interna de la app. Si accidentalmente lo conectas a la red de Traefik o expones el puerto 5432, cualquiera puede intentar conectarse.

6. Usa pg_dump, no copias de archivos

Copiar los archivos de datos de PostgreSQL mientras corre puede producir corrupción. Siempre usa pg_dump para backups. Es atomic y consistente.

7. Monitorea el tamaño de la base de datos

Con SELECT pg_size_pretty(pg_database_size('mydb')); puedes ver cuánto pesa cada base. Hemos encontrado bases de datos que crecieron inesperadamente porque un job de logs no tenía rotación.

8. Configura pg_stat_statements para debugging

Esta extensión registra estadísticas de todas las queries ejecutadas. Invaluable para encontrar queries lentas o frecuentes que podrían beneficiarse de caché o índices.

9. Ten un script de restore probado

Un backup sin proceso de restore probado es inútil. Al menos una vez al mes, restauramos un backup en un contenedor temporal para verificar que funciona: cat backup.sql | docker exec -i postgres-test psql -U myuser mydb.

10. Usa CUIDs o UUIDs, no secuenciales

IDs secuenciales (1, 2, 3) revelan información (cuántos registros hay, el orden de creación) y son predecibles. Usamos CUIDs de Prisma: @id @default(cuid()). Son únicos, no secuenciales, y seguros para exponer en URLs.

Si necesitas una app con base de datos robusta y todas estas lecciones ya aplicadas, cotiza tu proyecto en iAmanos aquí.

Caso Real: El Schema de WouWou — 25+ Modelos en Producción

Para que veas cómo se ve un schema Prisma 7 con PostgreSQL en una app compleja de producción, aquí está un resumen del schema de WouWou, nuestro SaaS veterinario:

Modelos principales (25+)

  • Clinic: nombre, slug, dirección, teléfono, horarios (jsonb), configuración (jsonb), plan, logoUrl
  • User: email, passwordHash, role (admin/vet/receptionist), clinicId (relación)
  • Pet: nombre, especie, raza (relación a Breed), peso, fechaNacimiento, foto, ownerId
  • Owner: nombre, teléfono, email, dirección, notas, clinicId
  • Appointment: fecha, hora, duración, status (scheduled/confirmed/completed/cancelled), petId, vetId, serviceId, notas
  • SOAPNote: subjective, objective, assessment, plan, appointmentId — el estándar médico para consultas
  • Prescription: medicamento, dosis, frecuencia, duración, notas, soapNoteId
  • BreedContent: slug, name, origin, size, lifespan, temperament (jsonb), care (jsonb), health (jsonb), imageUrl — 206 fichas completas
  • Service: nombre, descripción, precio, duración, categoría, tipo (consulta/estética/tienda)
  • Product: nombre, descripción, precio, stock, categoría, imageUrl
  • ChatMessage: role (user/assistant), content, userId, sessionId, toolCalls (jsonb) — historial del chat IA “Ayu”
  • Vaccination: vacuna, fecha, próximaDosis, petId, vetId

Relaciones clave

Clinic → Users, Pets, Owners, Appointments, Services, Products (todo filtrado por clínica). Pet → Owner (many-to-one), Appointments (one-to-many), Vaccinations (one-to-many). Appointment → SOAPNote (one-to-one), Prescription (one-to-many). Estas relaciones en Prisma se definen declarativamente y generan tipos TypeScript automáticos.

Uso de jsonb

Los campos jsonb en WouWou almacenan datos semi-estructurados que varían entre registros: horarios de clínica (diferentes por día), configuraciones (features habilitados, paleta de colores, datos fiscales), contenido de razas (temperament traits, care instructions, health alerts). PostgreSQL indexa estos campos y permite queries dentro del JSON sin perder la estructura relacional del resto del schema.

Este schema soporta toda la operación de una clínica veterinaria: citas, consultas médicas, historial de mascotas, inventario, facturación, y chat IA. Todo en PostgreSQL, todo tipado con Prisma 7, todo corriendo en Docker en un VPS de $200/mes.

La lección más importante de este caso: un schema bien diseñado con Prisma 7 y PostgreSQL escala naturalmente. Empezamos WouWou con 8 modelos y fue creciendo orgánicamente hasta 25+ sin necesidad de migrar o reestructurar. Prisma genera las migraciones automáticamente y PostgreSQL maneja el volumen de datos sin pestañear. Esto no hubiera sido posible con SQLite — los problemas de concurrencia habrían aparecido desde las primeras 10 citas simultáneas.

Resumen: Por Qué la Migración Vale Cada Hora Invertida

Si estás leyendo esto y todavía usas SQLite en producción, aquí está el argumento resumido para migrar:

  • Concurrencia: De “SQLITE_BUSY” con 15 usuarios a manejar miles de requests simultáneos sin problemas
  • Acceso remoto: De SSH + sqlite3 CLI a pgAdmin, DBeaver, Prisma Studio desde cualquier lugar
  • Backups: De copiar archivos con riesgo de corrupción a pg_dump atómico y consistente
  • Features: jsonb, full-text search en español, extensiones (PostGIS, pg_trgm), triggers, vistas materializadas
  • Escalabilidad: De un archivo que se bloquea a una base de datos que maneja millones de filas
  • Costo: $0 adicional si ya tienes un VPS con Docker (PostgreSQL es open source y gratuito)

El costo de la migración es de 2-4 horas por app. El costo de NO migrar es lidiar con limitaciones de SQLite cada día que tu app crece. La matemática es obvia.

En iAmanos hicimos esta migración para 15+ apps y no nos arrepentimos ni un segundo. Cada app es más estable, más rápida en concurrencia, y más fácil de debuggear. PostgreSQL + Prisma 7 es el estándar de oro para SaaS en 2026, y no vemos razón para usar otra cosa.

Si estás empezando un proyecto nuevo hoy, no pierdas tiempo evaluando opciones. Instala PostgreSQL en Docker, configura Prisma 7 con PrismaPg, define tu schema, ejecuta prisma db push, y empieza a construir. La decisión de base de datos no debería consumir más de 15 minutos de tu tiempo — PostgreSQL es la respuesta correcta para el 95% de los SaaS en México, y el 5% restante son casos tan específicos que ya sabrías si eres parte de ellos.

Cada día que pasas evaluando bases de datos es un día que no estás construyendo tu producto. Y el mercado no espera a que tomes la decisión perfecta — espera a que entregues valor. PostgreSQL + Prisma 7 entrega valor desde el día 1.

Preguntas Frecuentes

¿Prisma 7 es estable para producción?

Sí. Usamos Prisma 7 con el adapter PrismaPg en 15+ apps en producción sin problemas. El adapter pattern es la forma recomendada por Prisma para nuevos proyectos. La generación de tipos es robusta y las queries son eficientes.

¿Cuánta RAM consume PostgreSQL en Docker?

Un contenedor PostgreSQL 16 Alpine con configuración default consume ~50-100 MB de RAM. Con 15 bases de datos, usamos ~1-1.5 GB de RAM solo en PostgreSQL. En un VPS de 8 GB es perfectamente manejable.

¿Se puede usar PostgreSQL gratis en producción?

Sí, PostgreSQL es open source y gratuito. Si lo corres en Docker en tu propio VPS, no pagas nada adicional. Solo pagas el VPS que ya estás pagando. Services managed como Supabase o AWS RDS cobran extra.

¿Cómo hago backup de PostgreSQL en Docker?

Con pg_dump: docker exec postgres-myapp pg_dump -U myuser mydb > backup.sql. Programa esto con un cron job diario. Para restaurar: cat backup.sql | docker exec -i postgres-myapp psql -U myuser mydb.

¿Prisma 7 funciona con SQLite todavía?

Sí, Prisma 7 soporta SQLite, PostgreSQL, MySQL, MongoDB, y CockroachDB. Puedes empezar con SQLite y migrar a PostgreSQL después. Pero nuestra recomendación es empezar con PostgreSQL directamente para evitar la migración.