Skip to main content

terral-architecture

Arquitectura completa de TerrAn — ERP municipal con vista 3D, gestión documental, IA y datos en tiempo real. Referencia para hardware, escalabilidad y patrones.

Ir a la instalación

Datos de origen

Repositorio
Ntizar/NtizarBrainMasterMind
Última actividad en el origen
26 de junio de 2026 a las 12:05
Idioma detectado de SKILL.md
español
Estrellas
2
Forks
0

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Explorador de archivos
9 archivos

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
terral-architecture
description
Arquitectura completa de TerrAn — ERP municipal con vista 3D, gestión documental, IA y datos en tiempo real. Referencia para hardware, escalabilidad y patrones.
version
2.0.0
author
David Antizar
tags
["erp","municipal","saas","3d","gis","postgis","threejs","ai","rag","terran"]
# TerrAn — Arquitectura de Referencia ## Qué es TerrAn **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. ## 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 | | **PDF extraction** | pdf-parse | — | Extraer texto de PDFs | | **Deploy** | Docker + NaN Builders | — | Contenedor aislado | | **Object Storage** | MinIO | — | Alternativa self-hosted a S3 | ## 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) ⚠️ **La versión v1 del schema (CHECK constraint con 7 roles fijos + tabla `permisos` con `alcance` como VARCHAR libre) tiene estos problemas graves:** 1. **Roles fijos en CHECK no escalan a multi-tenant** — Cada ayuntamiento necesita roles distintos. No todos tienen alcalde, concejal, inspector. 2. **`permisos.alcance` como VARCHAR(100) plano** — Sin jerarquía, sin validación, se rompe silenciosamente con typos. 3. **Sin herencia de permisos** — Un `operario` de policía no hereda "ver activos" de su rol ni de su departamento. 4. **Sin distinción VER vs EDITAR** — Un alcalde debe poder ver todo pero NO editar salarios ni crear usuarios. 5. **Sin RLS real** — El filtrado está en middleware Node.js, no en PostgreSQL. Un bug en la API expone todos los datos. 6. **Sin lock release automático** — Si un usuario abre un activo, lo bloquea con optimistic locking, y se va → el activo queda bloqueado para siempre. #### 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 ```sql -- ============================================ -- 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, -- 'alcalde', 'concejal', 'inspector_medioambiental' nivel INTEGER NOT NULL DEFAULT 0, -- 0=admin, 10=directivo, 50=operario, 99=lectura hereda_de UUID REFERENCES roles_organizacion(id), -- Herencia: operario hereda de lectura es_admin BOOLEAN DEFAULT false, -- Si true, puede overridear TODO 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), -- Qué recurso recurso_categoria VARCHAR(100) NOT NULL, -- 'contenedor', 'ambulancia', 'doctor', 'usuario', 'documento' -- Niveles de permiso (jerárquicos: READ < WRITE < DELETE < ADMIN) permiso_nivel VARCHAR(20) NOT NULL CHECK (permiso_nivel IN ( 'none', -- Sin acceso (explícito) 'read', -- Solo ver 'write', -- Ver + crear + editar (implica read) 'delete', -- Ver + editar + borrar (implica write) 'admin' -- Todo + conceder a otros (implica delete) )), -- Alcance geográfico/departamental alcance_tipo VARCHAR(30) NOT NULL CHECK (alcance_tipo IN ( 'global', -- Todo el municipio 'departamento', -- 'policia', 'sanidad', 'urbanismo' 'zona', -- 'centro', 'norte', 'poligono_industrial' 'propio' -- Solo activos que creó o tiene asignados )), alcance_valor VARCHAR(100), -- Valor concreto: 'policia', 'centro', etc. NULL si alcance_tipo = 'global' created_at TIMESTAMPTZ DEFAULT now(), UNIQUE (rol_id, recurso_categoria, alcance_tipo, COALESCE(alcance_valor, '__global__')) ); -- ============================================ -- PERMISOS ESPECIALES (ad-hoc, para excepciones) -- ============================================ CREATE TABLE permisos_usuario ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), usuario_id UUID NOT NULL REFERENCES usuarios(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, alcance_valor VARCHAR(100), granted_by UUID NOT NULL REFERENCES usuarios(id), expira_en TIMESTAMPTZ, -- NULL = permanente motivo TEXT, created_at TIMESTAMPTZ DEFAULT now(), UNIQUE (usuario_id, recurso_categoria, alcance_tipo, COALESCE(alcance_valor, '__global__')) ); -- ============================================ -- RLS: aislamiento REAL en PostgreSQL -- ============================================ -- Función helper CREATE OR REPLACE FUNCTION app_current_org_id() RETURNS UUID AS $$ SELECT NULLIF(current_setting('app.org_id', true), '')::UUID; $$ LANGUAGE SQL STABLE; CREATE OR REPLACE FUNCTION app_current_user_id() RETURNS UUID AS $$ SELECT NULLIF(current_setting('app.user_id', true), '')::UUID; $$ LANGUAGE SQL STABLE; CREATE OR REPLACE FUNCTION app_is_admin() RETURNS BOOLEAN AS $$ SELECT current_setting('app.is_admin', true) = 'true'; $$ LANGUAGE SQL STABLE; -- RLS en activos ALTER TABLE activos ENABLE ROW LEVEL SECURITY; -- Policy base: solo activos de mi organización CREATE POLICY activos_org ON activos FOR ALL USING (org_id = app_current_org_id()); -- Policy para admin: puede ver/editar todo en su org CREATE POLICY activos_admin ON activos FOR ALL USING (app_is_admin() AND org_id = app_current_org_id()) WITH CHECK (true); -- Policy para operario con alcance de zona CREATE POLICY activos_zona ON activos FOR ALL USING ( org_id = app_current_org_id() AND ( -- El usuario tiene alcance_zona = valor de activos.zona EXISTS ( SELECT 1 FROM permisos_usuario pu JOIN usuarios u ON u.id = pu.usuario_id WHERE u.id = app_current_user_id() AND pu.alcance_tipo = 'zona' AND (activos.metadata->>'zona') = pu.alcance_valor AND pu.permiso_nivel IN ('write', 'delete', 'admin') ) OR app_is_admin() ) ); -- ============================================ -- LOCK RELEASE AUTOMÁTICO -- ============================================ ALTER TABLE activos ADD COLUMN IF NOT EXISTS locked_by UUID REFERENCES usuarios(id); ALTER TABLE activos ADD COLUMN IF NOT EXISTS locked_at TIMESTAMPTZ; -- Función de liberación automática (ejecutar cada 5 min vía cron) CREATE OR REPLACE FUNCTION release_stale_locks() RETURNS INTEGER AS $$ DECLARE released INTEGER; BEGIN UPDATE activos SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < now() - INTERVAL '10 minutes' AND deleted_at IS NULL; GET DIAGNOSTICS released = ROW_COUNT; RETURN released; END; $$ LANGUAGE plpgsql; ``` #### Middleware de autorización (Node.js) ```javascript // middleware/rls-context.js // Antes de cada request, inyecta el contexto en PostgreSQL para RLS async function setRLSContext(req, res, next) { if (req.user) { await pool.query(`SELECT set_config('app.org_id', $1, true)`, [req.user.org_id]); await pool.query(`SELECT set_config('app.user_id', $1, true)`, [req.user.id]); await pool.query(`SELECT set_config('app.is_admin', $1, true)`, [req.user.es_admin ? 'true' : 'false']); } next(); } // middleware/permission-check.js // Verificación adicional a nivel de aplicación (no solo RLS) function requirePermission(categoria, nivelMinimo, alcanceTipo = null) { return async (req, res, next) => { if (req.user.es_admin) return next(); // ADMIN puede TODO const tienePermiso = await checkPermission( req.user.id, categoria, nivelMinimo, alcanceTipo, req.params.zona ); if (!tienePermiso) { return res.status(403).json({ error: 'permiso_denegado', message: `No tienes permiso para ${accionNivel(nivelMinimo)} ${categoria}`, required: { categoria, nivelMinimo, alcance: alcanceTipo } }); } next(); }; } ``` #### Verificación de permisos en Node.js ```javascript // services/permission-query.js async function checkPermission(userId, categoria, nivelMinimo, alcanceTipo, alcanceValor) { const niveles = { 'none': 0, 'read': 1, 'write': 2, 'delete': 3, 'admin': 4 }; const minNivel = niveles[nivelMinimo];
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub