TerrAn — Arquitectura de Referencia
Qué es TerrAn
Territorio + Antizar (+ NaN builders). ERP municipal con vista 3D donde ayuntamientos y empresas gestionan activos físicos y humanos sobre un mapa interactivo de España. Datos en tiempo real (clima, trenes, cámaras), búsqueda semántica con IA, y gestión documental completa.
Stack tecnológico
| Capa |
Tecnología |
Versión |
Justificación |
| Backend |
Node.js + Express |
20 LTS |
Rápido de desarrollar, ESM |
| BD principal |
PostgreSQL + PostGIS |
16+ |
GIS nativo, particiones, FTS |
| Cache |
Redis |
7+ |
KPIs, sesiones, posiciones |
| Documentos |
MinIO (S3 self-hosted) |
Latest |
PDFs/fotos, no en BD |
| Búsqueda semántica |
ChromaDB |
Latest |
Embeddings, RAG |
| IA |
qwen3.6 vía NaN API |
— |
Asistente conversacional |
| Frontend 3D |
Three.js |
0.163+ |
Terreno, activos, LOD |
| Frontend UI |
Vanilla JS + Aurora CSS |
— |
Estilo David, sin framework |
| WebSocket |
ws (Node.js) |
— |
Real-time |
| Deploy |
Docker + NaN Builders |
— |
Contenedor aislado |
Hardware mínimo recomendado
Desarrollo local
- 4GB RAM, 2 cores, 40GB disco
- PostgreSQL + Redis + MinIO + Node.js = ~2GB RAM total
- ChromaDB = ~500MB RAM adicional
Producción (1 ayuntamiento pequeño-medium)
- Servidor: 8GB RAM, 4 cores, 200GB SSD
- PostgreSQL: 4GB RAM, 100GB SSD (con particiones)
- Redis: 1GB RAM
- MinIO: 50GB (documentos)
- Node.js: 2GB RAM (2 instancias para HA)
- Total estimado: ~8GB RAM, 4 cores, 200GB SSD
Producción (multi-tenant, 10+ ayuntamientos)
- Servidor: 32GB RAM, 8 cores, 1TB SSD
- PostgreSQL: 16GB RAM (read replica)
- Redis: 4GB RAM (cluster)
- MinIO: 500GB (documentos)
- Node.js: 8GB RAM (4 instancias, load balancer)
- Total estimado: ~32GB RAM, 8 cores, 1TB SSD
Patrones de arquitectura
Multi-tenant
- Cada ayuntamiento = una
organizacion con org_id
- TODAS las queries filtran por
org_id
- RLS (Row Level Security) en PostgreSQL para aislamiento
- Un usuario solo ve datos de su organización
Sistema de Permisos y RBAC (v2 — corregido tras auditoría)
Arquitectura RBAC corregida (3 capas)
Capa 1: PLATAFORMA
┌─────────────────────────────────┐
│ superadmin │
│ • Gestiona toda la plataforma │
│ • Crear organizaciones │
│ • Configurar tiers/precios │
│ • Ver logs de todas las orgs │
└─────────────────────────────────┘
Capa 2: ORGANIZACIÓN (por tenant)
┌─────────────────────────────────┐
│ admin_org (1 por organización) │
│ • Puede TODO en su org │
│ • Crear/editar/eliminar │
│ CUALQUIER recurso │
│ • Gestionar usuarios locales │
│ • Definir roles personalizados │
│ • Ver audit logs de su org │
│ • Override de optimistic locks │
├─────────────────────────────────┤
│ roles_personalizados │
│ (cada organización define │
│ sus propios roles con │
│ permisos asignables) │
└─────────────────────────────────┘
Capa 3: DATOS
┌─────────────────────────────────┐
│ RLS en PostgreSQL (obligatorio)│
│ • org_id en TODAS las queries │
│ • RLS policies por tabla │
│ • current_setting('app.*') │
└─────────────────────────────────┘
Schema de permisos corregido
-- ROLES: ya no son CHECK fijo, son por organización
CREATE TABLE roles_organizacion (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL REFERENCES organizaciones(id),
nombre VARCHAR(100) NOT NULL,
nivel INTEGER NOT NULL DEFAULT 0,
hereda_de UUID REFERENCES roles_organizacion(id),
es_admin BOOLEAN DEFAULT false,
activo BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (org_id, nombre)
);
-- PERMISOS: estructurados, no VARCHAR mágico
CREATE TABLE permisos_rol (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rol_id UUID NOT NULL REFERENCES roles_organizacion(id),
recurso_categoria VARCHAR(100) NOT NULL,
permiso_nivel VARCHAR(20) NOT NULL CHECK (permiso_nivel IN ('none','read','write','delete','admin')),
alcance_tipo VARCHAR(30) NOT NULL CHECK (alcance_tipo IN ('global','departamento','zona','propio')),
alcance_valor VARCHAR(100),
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (rol_id, recurso_categoria, alcance_tipo, COALESCE(alcance_valor, '__global__'))
);
Optimistic locking (v2)
- Campo
version en cada registro activo
- NUEVO:
locked_by + locked_at para saber QUIÉN lo está editando
- NUEVO: Cron cada 5 min libera locks > 10 min
Plugin architecture
- Cada dominio es un módulo independiente
- Módulo = { assetTypes, profiles, kpis, hooks, routes, dashboard }
- Se cargan dinámicamente según config del tenant
Hot/Warm/Cold
- Hot (0-3 meses): SSD, queries < 50ms
- Warm (3-12 meses): disco estándar, queries < 500ms
- Cold (> 12 meses): comprimido, acceso bajo demanda
Filosofía de desarrollo
Planificar ANTES de codear
Meses de planificación antes de escribir código. Prioridad:
- Schema completo de BD
- API REST diseñada (OpenAPI)
- Prototipos visuales (sin backend)
- Hosting definido
- Modelo de negocio
- SÓLO ENTONCES empezar a codear
Nombres en español
NUNCA inglés para proyectos propios de David. "TerrAn" = Territorio + Antizar + NaN.
Evaluar herramientas antes de integrar
- Qué hace la herramienta
- Qué necesita TerrAn que la herramienta NO cubre
- Qué componentes SÍ se pueden reutilizar
- Si construir encima aporta más de lo que complica
Referencias
references/gis-tool-evaluation.md — Patrón para evaluar herramientas GIS externas
references/erp-patterns.md — 10 patrones de diseño ERP
references/terran-audit-setup.md — Sistema de auto-auditoría cíclica
references/security-compliance-audit-checklist.md — 23 issues de seguridad y compliance
references/security-compliance-issues-detailed.md — 36 issues de seguridad detallados
references/usuarios-missing-org-id.md — Caso: tabla usuarios sin org_id (SEC-064)
references/business-logic-audit.md — 10 issues de lógica de negocio
references/business-logic-audit-iter161.md — Iter 161: 4 nuevos issues BL-011 a BL-014
references/audit-iteration-165.md — Iter 165: 82 issues en seguridad
references/audit-iteration-181.md — Iter 181: 12 nuevos issues (SEC-096 a SEC-101, BL-015 a BL-020)
Pitfalls críticos
- No asumir "gemelo digital 3D" — El 80% de TerrAn es backend ERP, no 3D
- PostgreSQL JSONB no es una DB dentro de la DB — normalizar datos consultados frecuentemente
- Roles fijos en CHECK no escalan — Usar tabla
roles_organizacion con org_id
- Permisos con VARCHAR libre es trampa — Usar
alcance_tipo ENUM + alcance_valor VARCHAR
- Sin herencia de permisos = duplicación masiva — Usar
heredar_de
- Lock sin release = activos muertos —
locked_by + locked_at + cron de release
- RLS no es opcional — Middleware puede tener bugs. PostgreSQL RLS es la última línea de defensa
- Audit log sin org_id = fuga multi-tenant — Todos los logs deben tener org_id
- JWT sin revocación = acceso post-despedido — TTL 15 min + refresh + token_blacklist
- Ley 39/2015 + eIDAS obligatorios — Firma electrónica sin validez legal hace el sistema inútil
Pitfall: BIGSERIAL con doble coma (SEC-096, iter 181)
En las líneas 3050 y 3305 de ARQUITECTURA.md: BIGSERIAL PRIMARY KEY,, (doble coma). Error de sintaxis SQL que impide crear las tablas security_log y login_attempts. Verificación: grep -n ,, en ARQUITECTURA.md para encontrar dobles comas.
Pitfall: security_log/login_attempts sin org_id (SEC-097, iter 181)
Las tablas security_log y login_attempts NO tienen columna org_id pero tienen RLS policies que refieren app_current_org_id(). Las policies no funcionan porque la columna no existe. Solución: añadir org_id UUID NOT NULL y actualizar las policies.
Pitfall: DEFAULT_POLICIES en JS sin tabla BD (SEC-100, iter 181)
RENDIMIENTO-Y-NEGOCIO.md define DEFAULT_POLICIES en JavaScript pero no hay tabla retention_policies en BD. Las políticas están hardcodeadas en código, no son configurables por el cliente. Solución: crear tabla retention_policies (org_id, tabla, dias_retencion, accion, activo).
Pitfall: getTier() hardcodeado en JS (SEC-101, iter 181)
La función getTier() referencia tier.price, tier.maxActivos pero no hay tabla tiers en el schema. Todo está hardcodeado en JavaScript. Solución: crear tabla tiers + suscripciones + getTier() hace query real a BD.
Pitfall: superadmin_bypass debe cubrir TODAS las tablas (iter 144)
Cada tabla con RLS debe tener un superadmin_bypass. Un superadmin debe poder TODO en su organización.
Pitfall: activos_humanos sin org_id, sin RLS, sin encriptación (iter 144)
La tabla activos_humanos tiene datos sensibles (DNI, NSS, datos médicos) que DEBE tener: org_id, RLS enabled + policy, y encriptación de campos sensibles con pgcrypto.
Pitfall: RLS enabled SIN policy = todo bloqueado (iter 144)
Habilitar RLS sin crear políticas bloquea TODAS las operaciones. Cada ALTER TABLE ENABLE ROW LEVEL SECURITY debe ir inmediatamente seguido de su CREATE POLICY.
TerrAn Schema Fix — Resolución de Issues de Auditoría (absorbido de terran-schema-fix)
Procedimiento de 6 fases
El auditor cíclico de TerrAn (terran-audit-loop) encuentra issues en 6 fases:
| Fase |
ID |
Qué cubre |
| 01 |
DATA |
Schema, constraints, triggers, tablas de referencia |
| 02 |
PERM |
RBAC, roles_organizacion, RLS policies, inheritance |
| 03 |
ADV |
Partitioning, autovacuum, retention policies, tablespaces, triggers |
| 04 |
API |
Zod validation, Redis rate limiting, error middleware, CORS, Helmet, WebSocket |
| 05 |
PERF |
Redis cache strategy, connection pooling, Sharp, code splitting, advisory locks |
| 06 |
SEC |
argon2id/bcrypt hashing, GDPR export, trash can, ChromaDB isolation, HTTPS/TLS |
Flujo de trabajo
- Verificar
max_issues_per_phase en audit-state.json — subir a 50+ (default 20 es insuficiente para SEC)
- Identificar issues abiertos por fase desde
audit-state.json
- Aplicar fixes en 3 archivos de docs (NO en BD real):
ARQUITECTURA.md, DOCUMENTOS-Y-IA.md, RENDIMIENTO-Y-NEGOCIO.md
- Verificar fixes en docs con assertions
- Actualizar
audit-state.json — marcar issues como fixed
- Verificación final — contar issues abiertos restantes (debe ser 0)
Patrones SQL reutilizables
- CHECK regex:
CONSTRAINT chk_formato CHECK (columna ~ '^[a-z]+:[a-z0-9_-]+$')
- CHECK array no vacío:
CONSTRAINT chk_no_vacio CHECK (array_length(columna, 1) > 0)
- CHECK rango:
EXISTS (SELECT 1 FROM unnest(columna) WHERE val < MIN OR val > MAX) = false
- Trigger auto-incremento por grupo: COALESCE(MAX(version), 0) + 1
- Partitioning por rango: PARTITION BY RANGE (fecha) con tablas hijas por trimestre/año
- RLS policy por org_id:
USING (org_id = current_setting('app.current_org_id')::UUID)
- Partial UNIQUE:
UNIQUE(email) WHERE deleted_at IS NULL para reactivación de usuarios
Pitfalls críticos
- NO es repo git —
/root/workspace/geoasset sin .git
- Fixes en docs, no en BD real — TerrAn en fase de diseño
- Overlaps entre fases — SEC issues ya resueltos en DATA/PERM/API
- BIGSERIAL con doble coma (SEC-096) —
BIGSERIAL PRIMARY KEY,,
- security_log/login_attempts sin org_id (SEC-097) — añadir columna + actualizar policies
- password_history en texto plano (SEC-098) — encriptar con pgcrypto
- getTier() hardcodeado sin tabla tiers (SEC-101) — crear tabla tiers + suscripciones
- RLS policy referencia columna inexistente — verificar con
information_schema.columns
- NUNCA confiar en
status: "fixed" sin verificar en docs — doble verificación obligatoria
- Documentos inconsistentes entre sí — sincronizar ARQUITECTURA.md con RENDIMIENTO-Y-NEGOCIO.md
- CREATE POLICY y ON en misma línea — usar
line.split('ON')[1].split()[0]
- docs_content truncado a ~30KB — siempre leer archivos directamente con
read_file()
1---2name: terran-architecture3description: Arquitectura completa de TerrAn — ERP municipal con vista 3D, gestión documental, IA y datos en tiempo real. Referencia para hardware, escalabilidad, RBAC y patrones.4---56# TerrAn — Arquitectura de Referencia78## Qué es TerrAn910**Terr**itorio + **An**tizar (+ NaN builders). ERP municipal con vista 3D donde ayuntamientos y empresas gestionan activos físicos y humanos sobre un mapa interactivo de España. Datos en tiempo real (clima, trenes, cámaras), búsqueda semántica con IA, y gestión documental completa.1112## Stack tecnológico1314| Capa | Tecnología | Versión | Justificación |15|---|---|---|---|16| **Backend** | Node.js + Express | 20 LTS | Rápido de desarrollar, ESM |17| **BD principal** | PostgreSQL + PostGIS | 16+ | GIS nativo, particiones, FTS |18| **Cache** | Redis | 7+ | KPIs, sesiones, posiciones |19| **Documentos** | MinIO (S3 self-hosted) | Latest | PDFs/fotos, no en BD |20| **Búsqueda semántica** | ChromaDB | Latest | Embeddings, RAG |21| **IA** | qwen3.6 vía NaN API | — | Asistente conversacional |22| **Frontend 3D** | Three.js | 0.163+ | Terreno, activos, LOD |23| **Frontend UI** | Vanilla JS + Aurora CSS | — | Estilo David, sin framework |24| **WebSocket** | ws (Node.js) | — | Real-time |25| **Deploy** | Docker + NaN Builders | — | Contenedor aislado |2627## Hardware mínimo recomendado2829### Desarrollo local30- 4GB RAM, 2 cores, 40GB disco31- PostgreSQL + Redis + MinIO + Node.js = ~2GB RAM total32- ChromaDB = ~500MB RAM adicional3334### Producción (1 ayuntamiento pequeño-medium)35- **Servidor:** 8GB RAM, 4 cores, 200GB SSD36- **PostgreSQL:** 4GB RAM, 100GB SSD (con particiones)37- **Redis:** 1GB RAM38- **MinIO:** 50GB (documentos)39- **Node.js:** 2GB RAM (2 instancias para HA)40- **Total estimado:** ~8GB RAM, 4 cores, 200GB SSD4142### Producción (multi-tenant, 10+ ayuntamientos)43- **Servidor:** 32GB RAM, 8 cores, 1TB SSD44- **PostgreSQL:** 16GB RAM (read replica)45- **Redis:** 4GB RAM (cluster)46- **MinIO:** 500GB (documentos)47- **Node.js:** 8GB RAM (4 instancias, load balancer)48- **Total estimado:** ~32GB RAM, 8 cores, 1TB SSD4950## Patrones de arquitectura5152### Multi-tenant53- Cada ayuntamiento = una `organizacion` con `org_id`54- TODAS las queries filtran por `org_id`55- RLS (Row Level Security) en PostgreSQL para aislamiento56- Un usuario solo ve datos de su organización5758### Sistema de Permisos y RBAC (v2 — corregido tras auditoría)5960#### Arquitectura RBAC corregida (3 capas)6162```63Capa 1: PLATAFORMA64┌─────────────────────────────────┐65│ superadmin │66│ • Gestiona toda la plataforma │67│ • Crear organizaciones │68│ • Configurar tiers/precios │69│ • Ver logs de todas las orgs │70└─────────────────────────────────┘7172Capa 2: ORGANIZACIÓN (por tenant)73┌─────────────────────────────────┐74│ admin_org (1 por organización) │75│ • Puede TODO en su org │76│ • Crear/editar/eliminar │77│ CUALQUIER recurso │78│ • Gestionar usuarios locales │79│ • Definir roles personalizados │80│ • Ver audit logs de su org │81│ • Override de optimistic locks │82├─────────────────────────────────┤83│ roles_personalizados │84│ (cada organización define │85│ sus propios roles con │86│ permisos asignables) │87└─────────────────────────────────┘8889Capa 3: DATOS90┌─────────────────────────────────┐91│ RLS en PostgreSQL (obligatorio)│92│ • org_id en TODAS las queries │93│ • RLS policies por tabla │94│ • current_setting('app.*') │95└─────────────────────────────────┘96```9798#### Schema de permisos corregido99100```sql101-- ROLES: ya no son CHECK fijo, son por organización102CREATE TABLE roles_organizacion (103 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),104 org_id UUID NOT NULL REFERENCES organizaciones(id),105 nombre VARCHAR(100) NOT NULL,106 nivel INTEGER NOT NULL DEFAULT 0,107 hereda_de UUID REFERENCES roles_organizacion(id),108 es_admin BOOLEAN DEFAULT false,109 activo BOOLEAN DEFAULT true,110 created_at TIMESTAMPTZ DEFAULT now(),111 UNIQUE (org_id, nombre)112);113114-- PERMISOS: estructurados, no VARCHAR mágico115CREATE TABLE permisos_rol (116 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),117 rol_id UUID NOT NULL REFERENCES roles_organizacion(id),118 recurso_categoria VARCHAR(100) NOT NULL,119 permiso_nivel VARCHAR(20) NOT NULL CHECK (permiso_nivel IN ('none','read','write','delete','admin')),120 alcance_tipo VARCHAR(30) NOT NULL CHECK (alcance_tipo IN ('global','departamento','zona','propio')),121 alcance_valor VARCHAR(100),122 created_at TIMESTAMPTZ DEFAULT now(),123 UNIQUE (rol_id, recurso_categoria, alcance_tipo, COALESCE(alcance_valor, '__global__'))124);125```126127### Optimistic locking (v2)128- Campo `version` en cada registro activo129- **NUEVO:** `locked_by` + `locked_at` para saber QUIÉN lo está editando130- **NUEVO:** Cron cada 5 min libera locks > 10 min131132### Plugin architecture133- Cada dominio es un módulo independiente134- Módulo = { assetTypes, profiles, kpis, hooks, routes, dashboard }135- Se cargan dinámicamente según config del tenant136137### Hot/Warm/Cold138- Hot (0-3 meses): SSD, queries < 50ms139- Warm (3-12 meses): disco estándar, queries < 500ms140- Cold (> 12 meses): comprimido, acceso bajo demanda141142## Filosofía de desarrollo143144### Planificar ANTES de codear145Meses de planificación antes de escribir código. Prioridad:1461. Schema completo de BD1472. API REST diseñada (OpenAPI)1483. Prototipos visuales (sin backend)1494. Hosting definido1505. Modelo de negocio1516. SÓLO ENTONCES empezar a codear152153### Nombres en español154NUNCA inglés para proyectos propios de David. "TerrAn" = Territorio + Antizar + NaN.155156### Evaluar herramientas antes de integrar1571. Qué hace la herramienta1582. Qué necesita TerrAn que la herramienta NO cubre1593. Qué componentes SÍ se pueden reutilizar1604. Si construir encima aporta más de lo que complica161162## Referencias163164- `references/gis-tool-evaluation.md` — Patrón para evaluar herramientas GIS externas165- `references/erp-patterns.md` — 10 patrones de diseño ERP166- `references/terran-audit-setup.md` — Sistema de auto-auditoría cíclica167- `references/security-compliance-audit-checklist.md` — 23 issues de seguridad y compliance168- `references/security-compliance-issues-detailed.md` — 36 issues de seguridad detallados169- `references/usuarios-missing-org-id.md` — Caso: tabla usuarios sin org_id (SEC-064)170- `references/business-logic-audit.md` — 10 issues de lógica de negocio171- `references/business-logic-audit-iter161.md` — Iter 161: 4 nuevos issues BL-011 a BL-014172- `references/audit-iteration-165.md` — Iter 165: 82 issues en seguridad173- `references/audit-iteration-181.md` — Iter 181: 12 nuevos issues (SEC-096 a SEC-101, BL-015 a BL-020)174175### Pitfalls críticos176177- **No asumir "gemelo digital 3D"** — El 80% de TerrAn es backend ERP, no 3D178- **PostgreSQL JSONB no es una DB dentro de la DB** — normalizar datos consultados frecuentemente179- **Roles fijos en CHECK no escalan** — Usar tabla `roles_organizacion` con `org_id`180- **Permisos con VARCHAR libre es trampa** — Usar `alcance_tipo ENUM + alcance_valor VARCHAR`181- **Sin herencia de permisos = duplicación masiva** — Usar `heredar_de`182- **Lock sin release = activos muertos** — `locked_by` + `locked_at` + cron de release183- **RLS no es opcional** — Middleware puede tener bugs. PostgreSQL RLS es la última línea de defensa184- **Audit log sin org_id = fuga multi-tenant** — Todos los logs deben tener org_id185- **JWT sin revocación = acceso post-despedido** — TTL 15 min + refresh + token_blacklist186- **Ley 39/2015 + eIDAS obligatorios** — Firma electrónica sin validez legal hace el sistema inútil187188### Pitfall: BIGSERIAL con doble coma (SEC-096, iter 181)189190En las líneas 3050 y 3305 de ARQUITECTURA.md: `BIGSERIAL PRIMARY KEY,,` (doble coma). Error de sintaxis SQL que impide crear las tablas security_log y login_attempts. **Verificación:** grep -n `,,` en ARQUITECTURA.md para encontrar dobles comas.191192### Pitfall: security_log/login_attempts sin org_id (SEC-097, iter 181)193194Las tablas security_log y login_attempts NO tienen columna org_id pero tienen RLS policies que refieren `app_current_org_id()`. Las policies no funcionan porque la columna no existe. **Solución:** añadir `org_id UUID NOT NULL` y actualizar las policies.195196### Pitfall: DEFAULT_POLICIES en JS sin tabla BD (SEC-100, iter 181)197198RENDIMIENTO-Y-NEGOCIO.md define DEFAULT_POLICIES en JavaScript pero no hay tabla retention_policies en BD. Las políticas están hardcodeadas en código, no son configurables por el cliente. **Solución:** crear tabla retention_policies (org_id, tabla, dias_retencion, accion, activo).199200### Pitfall: getTier() hardcodeado en JS (SEC-101, iter 181)201202La función getTier() referencia `tier.price`, `tier.maxActivos` pero no hay tabla tiers en el schema. Todo está hardcodeado en JavaScript. **Solución:** crear tabla tiers + suscripciones + getTier() hace query real a BD.203204### Pitfall: superadmin_bypass debe cubrir TODAS las tablas (iter 144)205206Cada tabla con RLS debe tener un superadmin_bypass. Un superadmin debe poder TODO en su organización.207208### Pitfall: activos_humanos sin org_id, sin RLS, sin encriptación (iter 144)209210La tabla activos_humanos tiene datos sensibles (DNI, NSS, datos médicos) que DEBE tener: org_id, RLS enabled + policy, y encriptación de campos sensibles con pgcrypto.211212### Pitfall: RLS enabled SIN policy = todo bloqueado (iter 144)213214Habilitar RLS sin crear políticas bloquea TODAS las operaciones. Cada `ALTER TABLE ENABLE ROW LEVEL SECURITY` debe ir inmediatamente seguido de su `CREATE POLICY`.215216## TerrAn Schema Fix — Resolución de Issues de Auditoría (absorbido de `terran-schema-fix`)217218### Procedimiento de 6 fases219El auditor cíclico de TerrAn (`terran-audit-loop`) encuentra issues en 6 fases:220| Fase | ID | Qué cubre |221|------|-----|-----------|222| 01 | DATA | Schema, constraints, triggers, tablas de referencia |223| 02 | PERM | RBAC, roles_organizacion, RLS policies, inheritance |224| 03 | ADV | Partitioning, autovacuum, retention policies, tablespaces, triggers |225| 04 | API | Zod validation, Redis rate limiting, error middleware, CORS, Helmet, WebSocket |226| 05 | PERF | Redis cache strategy, connection pooling, Sharp, code splitting, advisory locks |227| 06 | SEC | argon2id/bcrypt hashing, GDPR export, trash can, ChromaDB isolation, HTTPS/TLS |228229### Flujo de trabajo2301. **Verificar `max_issues_per_phase`** en `audit-state.json` — subir a 50+ (default 20 es insuficiente para SEC)2312. **Identificar issues abiertos** por fase desde `audit-state.json`2323. **Aplicar fixes** en 3 archivos de docs (NO en BD real): `ARQUITECTURA.md`, `DOCUMENTOS-Y-IA.md`, `RENDIMIENTO-Y-NEGOCIO.md`2334. **Verificar fixes** en docs con assertions2345. **Actualizar `audit-state.json`** — marcar issues como `fixed`2356. **Verificación final** — contar issues abiertos restantes (debe ser 0)236237### Patrones SQL reutilizables238- **CHECK regex:** `CONSTRAINT chk_formato CHECK (columna ~ '^[a-z]+:[a-z0-9_-]+$')`239- **CHECK array no vacío:** `CONSTRAINT chk_no_vacio CHECK (array_length(columna, 1) > 0)`240- **CHECK rango:** `EXISTS (SELECT 1 FROM unnest(columna) WHERE val < MIN OR val > MAX) = false`241- **Trigger auto-incremento por grupo:** COALESCE(MAX(version), 0) + 1242- **Partitioning por rango:** PARTITION BY RANGE (fecha) con tablas hijas por trimestre/año243- **RLS policy por org_id:** `USING (org_id = current_setting('app.current_org_id')::UUID)`244- **Partial UNIQUE:** `UNIQUE(email) WHERE deleted_at IS NULL` para reactivación de usuarios245246### Pitfalls críticos247- **NO es repo git** — `/root/workspace/geoasset` sin `.git`248- **Fixes en docs, no en BD real** — TerrAn en fase de diseño249- **Overlaps entre fases** — SEC issues ya resueltos en DATA/PERM/API250- **BIGSERIAL con doble coma** (SEC-096) — `BIGSERIAL PRIMARY KEY,,`251- **security_log/login_attempts sin org_id** (SEC-097) — añadir columna + actualizar policies252- **password_history en texto plano** (SEC-098) — encriptar con pgcrypto253- **getTier() hardcodeado sin tabla tiers** (SEC-101) — crear tabla tiers + suscripciones254- **RLS policy referencia columna inexistente** — verificar con `information_schema.columns`255- **NUNCA confiar en `status: "fixed"` sin verificar en docs** — doble verificación obligatoria256- **Documentos inconsistentes entre sí** — sincronizar ARQUITECTURA.md con RENDIMIENTO-Y-NEGOCIO.md257- **CREATE POLICY y ON <table> en misma línea** — usar `line.split('ON')[1].split()[0]`258- **docs_content truncado a ~30KB** — siempre leer archivos directamente con `read_file()`