Base de datos
Qué documentar acá
Sección titulada «Qué documentar acá»- Modelo de datos — diagrama entidad-relación y qué representa cada tabla en términos del dominio.
- Migraciones — cómo se crean, cómo se aplican, cómo se revierte.
- Índices y consultas críticas — las consultas que tienen que ser rápidas sí o sí.
- Datos semilla — qué necesita una instalación limpia para arrancar.
El punto delicado
Sección titulada «El punto delicado»El modelo tiene que distinguir producto de envase retornable. Una paca sale
y no vuelve; un botellón sale, se espera que vuelva, y a veces no vuelve. Son dos
ciclos de vida distintos y un solo campo stock no los representa.
Esa decisión, cuando se tome, va documentada como ADR — es de las caras de revertir.
Estado actual
Sección titulada «Estado actual»M0 — Auth + RBAC: ✅ (20-ago-2026). M1 — Productos: ✅ (22-ago-2026). M2 — Stock: ✅ (22-ago-2026). M3 — Insumos, M4 — Producción, M5 — Clientes, M6 — Ventas, M7 — Retornables: ✅. M9 — Proveedores, M11 — Contador, M12 — Alertas: ✅. M13 — Auditoría: ✅ (transversal desde M0). M14 — Clientes ubicables: ✅ (10-sep-2026). M15 — Seguimientos: ✅ (21-sep-2026). M10 — Precios: ✅ absorbido por M1 y M6. M8 — Rutas y seller mobile: ⏸ POST-MVP (diferido).
30 tablas y 22 migraciones aplicadas, sobre PostgreSQL 17.
| Tabla | Para qué |
|---|---|
users |
Identidad. email es citext: el login es case-insensitive en la base, sin depender de que cada query recuerde un LOWER() |
accounts |
Acá vive el hash de la contraseña, no en users. Better-Auth separa identidad de credencial para poder sumar OAuth sin tocar usuarios |
sessions |
Con roles[] congelados al iniciar sesión (RN-ACC-01) |
verifications |
Tokens de un solo uso para recuperar la contraseña |
roles |
Catálogo cerrado de los cuatro |
user_roles |
N:M, con quién otorgó cada rol |
audit_log |
Bitácora append-only |
productos |
Catálogo. Precios con piso garantizado por CHECK y litros como columna generada |
lotes |
Producto empacado un día, con su saldo encima. FIFO por vencimiento |
movimientos_stock |
El libro que explica el saldo. Append-only, igual que audit_log |
productos: los invariantes viven en la base
Sección titulada «productos: los invariantes viven en la base»La tabla del catálogo (M1) apoya tres reglas del dominio en Postgres y no en la capa de servicio.
El criterio general está en ADR-0006: la base impide el dato malo aunque un endpoint se olvide, y el servicio existe para que el error sea legible. No son dos validaciones de lo mismo — son dos responsabilidades.
| Mecanismo | Qué garantiza |
|---|---|
CHECK productos_precio_minimo_es_piso |
RN-CAT-04 — el piso nunca supera un precio de lista |
GENERATED ALWAYS AS en litros |
RN-PRD-01 — el derivado no puede desincronizarse de contenido_ml × unidades |
REVOKE DELETE a aquazaku_app |
RN-CAT-02 — un producto se desactiva, no se borra |
El servicio igual valida el piso, pero para otra cosa: para que el error diga qué corregir en vez de escupir un mensaje de Postgres. La base es la que impide el dato malo aunque un endpoint se olvide.
lotes y movimientos_stock: el saldo y el libro que lo explica
Sección titulada «lotes y movimientos_stock: el saldo y el libro que lo explica»El saldo vive en el lote, no en una tabla aparte: un lote ya es “producto + fecha”, y una tabla de saldos solo repetiría esa clave para agregar un número.
Los dos se escriben en la misma transacción. Si el saldo baja y el movimiento no queda, el libro deja de explicar el saldo — es la primera forma de descuadre, y la más difícil de rastrear meses después.
| Mecanismo | Qué garantiza |
|---|---|
CHECK cantidad_disponible >= 0 |
RN-STK-03 — no hay stock negativo |
REVOKE UPDATE, DELETE en movimientos_stock |
RN-STK-02 — un libro editable no es un libro |
REVOKE DELETE en lotes |
Un lote no se borra, se queda en cero. Borrarlo dejaría movimientos huérfanos y ventas sin trazabilidad |
CHECK condicionales de motivo y causa |
RN-STK-02 y RN-STK-06 — exigen el dato solo en ajuste y descarte |
audit_log es inmutable de verdad
Sección titulada «audit_log es inmutable de verdad»Dos mecanismos independientes, porque ninguno alcanza solo — ADR-0004:
- Triggers que rechazan
UPDATE,DELETEyTRUNCATE. SonFOR EACH STATEMENT: unDELETEque no matchea ninguna fila nunca dispararía uno por fila y pasaría en silencio. - El rol de la aplicación (
aquazaku_app) no es dueño de las tablas y sobreaudit_logsolo tieneSELECTeINSERT.
Al dueño lo frena el trigger; al trigger lo podría desactivar el dueño. Juntas,
adulterar la bitácora exige credenciales del rol dueño y un ALTER TABLE
explícito.
audit_log no tiene foreign key a users, a propósito: si un usuario se
borra, su rastro tiene que sobrevivir. Un log que se borra en cascada no es un
log.
Dos roles, no uno
Sección titulada «Dos roles, no uno»| Rol | Lo usa | Puede |
|---|---|---|
aquazaku |
drizzle-kit y el runner de migraciones | Dueño de las tablas |
aquazaku_app |
el servidor en runtime | CRUD sobre datos; sobre audit_log, solo leer e insertar |
Se traduce en dos variables: DATABASE_URL y DATABASE_MIGRATION_URL. El
servidor nunca abre una conexión con la segunda. Provisionamiento en
Entorno local.
Migraciones y seed
Sección titulada «Migraciones y seed»pnpm db:migrate # base de desarrollopnpm db:migrate:test # base de testspnpm db:seed # roles, primer admin y catálogo de productos| Migración | Qué trae |
|---|---|
0000_m0_initial |
Las siete tablas de M0 |
0001_audit_append_only |
Triggers de audit_log + permisos de aquazaku_app |
0002_productos |
Catálogo de productos, sus CHECK y el REVOKE DELETE |
0003_stock |
Lotes con saldo, libro de movimientos append-only y sus REVOKE |
Las migraciones son SQL explícito de Drizzle Kit, nunca auto-generadas contra producción. Verificado: corren sobre una base virgen sin pasos manuales y son idempotentes.
El seed también es idempotente: si ya hay un administrador activo no hace nada y
termina con éxito, así puede vivir en el pipeline de deploy. En producción
exige SEED_CONFIRM=yes.
Decisiones de modelado
Sección titulada «Decisiones de modelado»Las reglas de negocio viven en /dominio/ y no se duplican acá.
