Capítulo 1: Visión general de la arquitectura
El sistema ayuntamientosmart.com sigue una arquitectura de tres capas sin frameworks, optimizada para simplicidad operativa y bajo acoplamiento:
Capa de presentación
Responsable de la interfaz de usuario y la interacción con el usuario final. Compuesta por:
- Archivos PHP que generan HTML5 responsive
- CSS3 puro sin preprocesadores (diseño mobile-first)
- JavaScript vanilla sin librerías pesadas (solo fetch API nativa)
- Plantillas HTML reutilizables
Capa de lógica de negocio
Implementa las reglas de negocio, validaciones y orquestación de operaciones. Ubicada en:
- /inc/ - Componentes core reutilizables
- /modulos/{modulo}/ - Lógica específica de cada módulo
- Funciones puras cuando es posible (sin estado)
- Sistema de eventos para extensibilidad
Capa de datos
Gestiona el almacenamiento, recuperación y persistencia de datos:
- Motor JSON DB con hash partitioning (256 buckets)
- Sistema de caché en memoria (APCu/Redis)
- File locking para concurrencia (flock)
- Backup automático incremental
Capítulo 2: Estructura de directorios
La estructura de directorios del sistema está organizada de forma lógica y predecible:
/
├── index.php # Punto de entrada: login y dashboard
├── ajax.php # Manejador de peticiones AJAX
├── api.php # Gateway API REST con autenticación JWT
├── cron.php # Tareas programadas (backups, notificaciones)
├── inc/ # Componentes core del sistema
│ ├── config.php # Constantes: paths, keys, timeouts
│ ├── core.php # Utilidades base, sesiones, tenant
│ ├── json_db.php # Motor de base de datos JSON
│ ├── hash.php # Hash partitioning (256 buckets)
│ ├── auth.php # Autenticación Argon2id, 2FA, brute-force
│ ├── permisos.php # RBAC con 6 roles y wildcards
│ ├── cifrado.php # AES-256-GCM para campos sensibles
│ ├── log.php # Auditoría con hash SHA256
│ └── validacion.php # Sanitización de inputs
├── modulos/ # Módulos funcionales (25 módulos)
│ ├── denuncias/
│ │ ├── index.php # Listado de denuncias
│ │ ├── ver.php # Detalle de denuncia
│ │ ├── nuevo.php # Crear denuncia
│ │ ├── editar.php # Editar denuncia
│ │ ├── ajax.php # AJAX específico del módulo
│ │ └── exportar.php # Exportar a PDF/Excel
│ ├── sanciones/
│ ├── atestados/
│ └── .../
├── data/ # Datos persistentes
│ ├── {tenant_hash}/ # Datos por tenant (multi-tenancy)
│ │ ├── _config.json # Configuración del tenant
│ │ ├── denuncias/ # Módulo denuncias
│ │ │ ├── 2026/ # Año
│ │ │ │ ├── 00.json # Bucket 00 (hash partition)
│ │ │ │ ├── 01.json # Bucket 01
│ │ │ │ ├── .../
│ │ │ │ └── ff.json # Bucket ff (256 buckets)
│ │ ├── sanciones/
│ │ └── .../
├── assets/ # Recursos estáticos
│ ├── css/
│ ├── js/
│ └── img/
└── vendor/ # Librerías externas (Composer)
Capítulo 3: Motor de base de datos JSON
El sistema utiliza almacenamiento basado en archivos JSON en lugar de bases de datos tradicionales (MySQL, PostgreSQL). Esta decisión arquitectónica tiene ventajas importantes:
Ventajas del almacenamiento JSON
- Simplicidad: No requiere servidor de base de datos separado, instalación ni configuración
- Portabilidad: Los datos son archivos de texto, fácilmente transportables, versionables con Git
- Backup sencillo: Copiar archivos, sin dumps complejos
- Inspección directa: Los datos son legibles por humanos
- Escalabilidad horizontal: Cada tenant tiene sus propios archivos, fácil distribuir en múltiples servidores
Hash partitioning
Para evitar archivos JSON excesivamente grandes, se utiliza hash partitioning con 256 buckets (00-ff):
function obtener_bucket($id) {
$hash = md5($id);
return substr($hash, 0, 2); // Primeros 2 caracteres hex: 00-ff
}
// Ejemplo: ID 'DEN-2026-00123'
// MD5: '5f2a8b...' → Bucket: '5f'
// Archivo: /data/{tenant}/denuncias/2026/5f.json
Con 256 buckets, si tenemos 25.600 registros, cada archivo contiene ~100 registros. Archivos de 100 registros son rápidos de leer/escribir.
File locking para concurrencia
Para evitar corrupción de datos con escrituras concurrentes, se usa flock():
$fp = fopen($archivo, 'c+');
if (flock($fp, LOCK_EX)) { // Lock exclusivo
$datos = json_decode(fread($fp, filesize($archivo)), true);
// Modificar $datos
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, json_encode($datos, JSON_PRETTY_PRINT));
fflush($fp);
flock($fp, LOCK_UN); // Liberar lock
}
fclose($fp);
Caché en memoria
Para mejorar el rendimiento, se cachean en memoria (APCu o Redis) los archivos JSON accedidos frecuentemente. La caché se invalida automáticamente cuando se modifica el archivo.
Capítulo 4: Componentes core
config.php - Configuración central
Define todas las constantes del sistema:
// Paths
define('ROOT_PATH', __DIR__ . '/..');
define('DATA_PATH', ROOT_PATH . '/data');
define('INC_PATH', ROOT_PATH . '/inc');
// Seguridad
define('ENCRYPTION_KEY', '...'); // AES-256 key (32 bytes)
define('SALT_TENANT', '...'); // Salt para hash de tenant
define('HASH_ALGO', 'sha256'); // Algoritmo hash para auditoría
// Sesiones
define('SESSION_TIMEOUT', 3600); // 1 hora
define('SESSION_NAME', 'AYUNTAMIENTOSMART_SID');
// Intentos de login
define('MAX_LOGIN_ATTEMPTS', 5);
define('LOGIN_LOCKOUT_TIME', 900); // 15 minutos
core.php - Utilidades base
Funciones de utilidad general:
- get_tenant(): Detecta el tenant desde el subdomain y calcula su hash
- session_manager(): Gestión de sesiones seguras con regeneración de ID
- redirect(): Redirecciones con código HTTP correcto
- json_response(): Genera respuestas JSON estándar para AJAX/API
- sanitize_filename(): Sanitiza nombres de archivo para prevenir path traversal
json_db.php - Motor de base de datos
API de alto nivel para operaciones CRUD:
// Leer registros
$registros = db_find('denuncias', ['estado' => 'registrada'], 2026);
// Crear registro
$id = db_insert('denuncias', $datos, 2026);
// Actualizar registro
db_update('denuncias', $id, $cambios, 2026);
// Eliminar registro (soft delete)
db_delete('denuncias', $id, 2026);
// Contar registros
$total = db_count('denuncias', ['tipo' => 'hechos'], 2026);
auth.php - Autenticación
Sistema de autenticación robusto:
- Hash de contraseñas: Argon2id con parámetros configurables (memory: 65536 KB, time: 4, threads: 3)
- 2FA opcional: TOTP (Google Authenticator, Authy)
- Brute-force protection: Bloqueo temporal tras X intentos fallidos
- Sesiones seguras: HttpOnly, Secure, SameSite cookies
- Logout automático: Por inactividad configurable
permisos.php - Control de acceso RBAC
Sistema de permisos basado en roles con wildcards:
// 6 roles predefinidos
$ROLES = [
'admin' => ['*'], // Acceso total
'jefe_servicio' => ['denuncias.*', 'sanciones.*', 'estadisticas.*'],
'concejal' => ['denuncias.ver', 'denuncias.crear', 'sanciones.*'],
'tecnico' => ['denuncias.crear', 'sanciones.crear'],
'administrativo'=> ['denuncias.ver', 'sanciones.tramitar'],
'consulta' => ['*.ver']
];
// Comprobar permiso
if (tiene_permiso($usuario, 'denuncias.crear')) {
// Permitir operación
}
cifrado.php - Cifrado de datos sensibles
Cifrado AES-256-GCM para campos sensibles (DNI, dirección, teléfono, datos médicos):
// Cifrar
$cifrado = cifrar_campo($dni); // Devuelve base64 del texto cifrado + IV + tag
// Descifrar
$dni = descifrar_campo($cifrado);
// Campos cifrados por módulo (constante)
$CAMPOS_CIFRADOS = [
'denuncias' => ['denunciante.dni', 'denunciante.direccion', 'denunciante.telefono'],
'sanciones' => ['conductor.dni', 'conductor.direccion'],
'atestados' => ['detenido.dni', 'detenido.antecedentes']
];
log.php - Sistema de auditoría
Logging completo de todas las operaciones:
log_auditoria(
$modulo, // 'denuncias', 'sanciones', etc.
$accion, // 'crear', 'modificar', 'eliminar', 'ver'
$registro_id, // ID del registro afectado
$usuario_id, // Usuario que realiza la acción
$datos_extra // Array con datos adicionales
);
// Cada entrada de log incluye:
// - Timestamp exacto (microsegundos)
// - Usuario y rol
// - IP de origen
// - Acción realizada
// - Datos antes/después (para modificaciones)
// - Hash SHA256 de la entrada anterior (cadena inmutable)
Capítulo 5: Multi-tenancy
El sistema soporta múltiples tenants (ayuntamientos) en la misma instalación mediante subdominios:
Detección de tenant
// URL: https://madrid.ayuntamientosmart.com/
$subdomain = 'madrid';
$tenant_hash = substr(hash('sha256', $subdomain . SALT_TENANT), 0, 8);
// tenant_hash: 'a3f5c2d1' (8 caracteres hex)
// Los datos del tenant se almacenan en:
// /data/a3f5c2d1/
Aislamiento de datos
Cada tenant tiene su propio directorio en /data/. No hay forma de que un tenant acceda a datos de otro. El hash del tenant se calcula con un salt secreto, por lo que no es predecible.
Configuración por tenant
Cada tenant tiene su archivo _config.json con configuración específica:
{
"municipio": "Ayuntamiento de Madrid",
"cif": "P2807900B",
"direccion": "Plaza de la Villa, 1",
"logo": "madrid.png",
"integraciones": {
"dgt_testra": true,
"lexnet": true,
"viogen": false
},
"limites": {
"usuarios_max": 50,
"almacenamiento_mb": 5000
}
}Capítulo 6: Sistema de módulos
El sistema es altamente modular. Cada módulo (denuncias, sanciones, etc.) es autocontenido en su directorio y sigue la misma estructura:
/modulos/denuncias/
├── index.php # Listado con filtros y paginación
├── ver.php # Vista detalle de un registro
├── nuevo.php # Formulario de creación
├── editar.php # Formulario de edición
├── ajax.php # Endpoints AJAX específicos del módulo
└── exportar.php # Exportación a PDF/Excel
Ventajas de esta arquitectura modular:
- Cada módulo es independiente y puede desarrollarse/testearse aisladamente
- Fácil añadir nuevos módulos sin tocar código existente
- Posibilidad de activar/desactivar módulos por tenant
- Claridad en la organización del código
Patrones de diseño utilizados
El sistema utiliza varios patrones de diseño reconocidos para mantener el código limpio, mantenible y escalable.
Repository pattern
La capa json_db.php actúa como repository abstracto que encapsula toda la lógica de acceso a datos. Los módulos no acceden directamente a los archivos JSON, sino a través de las funciones db_*. Esto permite cambiar la implementación del almacenamiento (por ejemplo, migrar a PostgreSQL) sin tocar el código de los módulos.
Dependency Injection
Las funciones reciben sus dependencias como parámetros en lugar de usar globales o singletons. Por ejemplo:
function crear_denuncia($datos, $usuario, $db_instance) {
// Validar
validar_denuncia($datos);
// Guardar
$id = $db_instance->insert('denuncias', $datos);
// Auditar
log_auditoria('denuncias', 'crear', $id, $usuario);
return $id;
}Esto facilita el testing (se pueden inyectar mocks) y reduce el acoplamiento.
Event-driven architecture
El sistema implementa un bus de eventos simple para permitir extensiones sin modificar el código core:
// Disparar evento
event_dispatch('denuncia.creada', ['id' => $id, 'datos' => $datos]);
// Suscribirse a evento (en plugin o módulo externo)
event_subscribe('denuncia.creada', function($payload) {
// Enviar email al denunciante
enviar_email_confirmacion($payload['datos']['email'], $payload['id']);
});Strategy pattern
Para las integraciones externas (DGT, LexNet, VioGén), se usa el patrón Strategy que permite intercambiar implementaciones:
interface IntegracionStrategy {
public function consultar($parametros);
public function enviar($datos);
}
class DGTStrategy implements IntegracionStrategy { ... }
class LexNetStrategy implements IntegracionStrategy { ... }
// Uso
$integracion = obtener_integracion('dgt'); // Devuelve instancia de DGTStrategy
$resultado = $integracion->consultar(['matricula' => '1234ABC']);Rendimiento y optimizaciones
El sistema implementa varias estrategias de optimización para garantizar tiempos de respuesta rápidos incluso con grandes volúmenes de datos.
Hash partitioning
Ya mencionado, el hash partitioning en 256 buckets distribuye los registros uniformemente. Esto evita archivos JSON de cientos de MB. Con hash partitioning, los archivos raramente superan 1 MB, lo que garantiza lecturas/escrituras rápidas.
Caché multinivel
Se implementan tres niveles de caché:
- Caché de archivos JSON (APCu): Los archivos JSON leídos se cachean en memoria durante 5 minutos. Si el archivo no cambia, se sirve desde RAM.
- Caché de consultas frecuentes (Redis): Consultas complejas (estadísticas, búsquedas) se cachean durante 1 hora.
- Caché HTTP de navegador: Recursos estáticos (CSS, JS, imágenes) con cache-control de 1 año.
Lazy loading
Los registros se cargan bajo demanda. Por ejemplo, en el listado de denuncias solo se cargan los campos básicos (id, fecha, denunciante, estado). Los datos completos (hechos, adjuntos, historial) solo se cargan al abrir el detalle. Esto reduce drásticamente el tamaño de las respuestas.
Paginación eficiente
Los listados están paginados con límite de 20-50 registros por página. La paginación se implementa a nivel de bucket: primero se identifican los buckets que contienen registros que cumplen los filtros, luego se cargan solo los buckets necesarios para la página actual.
Índices en memoria
Se mantienen índices ligeros en archivos separados para acelerar búsquedas. Por ejemplo: /data/{tenant}/denuncias/_index_estado.json contiene un mapa estado → [ids] que permite filtrar por estado sin cargar todos los buckets.
Seguridad en profundidad
La arquitectura del sistema incorpora seguridad en múltiples capas siguiendo el principio de defensa en profundidad.
Validación de entrada
Toda entrada de usuario pasa por validación y sanitización en validacion.php antes de procesarse:
- Validación de tipos (entero, email, fecha, DNI, matrícula)
- Sanitización de HTML para prevenir XSS
- Escape de caracteres especiales
- Longitud máxima de campos
- Whitelist de valores permitidos cuando aplica
Protección contra path traversal
Todos los paths de archivos se sanitizan para prevenir ataques de path traversal:
function sanitize_path($path) {
// Eliminar '..' y otros caracteres peligrosos
$path = str_replace(['..', '~'], '', $path);
$path = preg_replace('/[^a-z0-9_\/-]/i', '', $path);
return $path;
}Cifrado en reposo
Datos sensibles cifrados con AES-256-GCM. La clave de cifrado se almacena fuera del webroot y se carga desde variable de entorno. Cada campo cifrado usa un IV aleatorio único, previniendo ataques de diccionario.
Cifrado en tránsito
TLS 1.3 obligatorio. Configuración del servidor web con ciphers seguros (ECDHE, AES-GCM), HSTS habilitado, certificados de CA reconocidas (Let's Encrypt, Sectigo).
Auditoría inmutable
Los logs de auditoría son inmutables mediante hash encadenado. Cada entrada incluye el hash SHA256 de la entrada anterior, formando una cadena. Si se modifica una entrada histórica, se rompe la cadena y se detecta inmediatamente.
Backup y recuperación
El sistema implementa una estrategia completa de backup para garantizar la recuperabilidad ante desastres.
Backup incremental diario
Cada noche a las 03:00 AM (ejecutado por cron.php), se crea un backup incremental de todos los datos modificados en las últimas 24 horas. Los backups se almacenan en /backups/{tenant}/{fecha}/ comprimidos con gzip. Se conservan: diarios de último mes, semanales de último trimestre, mensuales de último año.
Backup completo semanal
Cada domingo a las 02:00 AM, se crea un backup completo de todos los datos del tenant. Esto permite restauración completa en caso de corrupción total. Los backups completos se conservan durante 3 meses.
Réplica en tiempo real
Opcionalmente (configuración del tenant), se puede activar réplica en tiempo real a servidor secundario. Cada escritura se replica asíncronamente vía rsync/SSH al servidor de backup. El lag típico es <1 minuto.
Procedimiento de restauración
Para restaurar desde backup:
- Detener el sistema (poner en modo mantenimiento)
- Identificar el backup a restaurar (/backups/{tenant}/{fecha}/)
- Extraer el backup: tar -xzf backup.tar.gz -C /data/{tenant}/
- Verificar integridad: comprobar hashes de auditoría
- Reiniciar el sistema
Tiempo típico de restauración: 5-15 minutos dependiendo del tamaño de datos.
Escalabilidad
Aunque el sistema está diseñado para municipios pequeños-medianos (hasta 50 usuarios concurrentes), incorpora mecanismos de escalabilidad para crecer si es necesario.
Escalado vertical
La arquitectura soporta fácilmente escalado vertical (más CPU, más RAM, más almacenamiento en el mismo servidor). Aumentar la RAM disponible para caché (APCu) mejora linealmente el rendimiento. Discos SSD NVMe reducen significativamente la latencia de I/O.
Escalado horizontal (multi-servidor)
Para cargas muy altas, se puede escalar horizontalmente:
- Balanceador de carga: Nginx/HAProxy distribuye peticiones entre N servidores web
- Almacenamiento compartido: /data/ en NFS o GlusterFS compartido entre servidores
- Caché compartida: Redis centralizado para caché compartida
- Sesiones compartidas: Sesiones en Redis para que cualquier servidor pueda atenderlas
Separación de tenants
En instalaciones grandes con muchos tenants, se pueden asignar tenants a servidores dedicados. Por ejemplo: madrid.ayuntamientosmart.com → servidor A, barcelona.ayuntamientosmart.com → servidor B. El balanceador rutea según subdomain. Esto proporciona aislamiento total entre tenants grandes.
CDN para estáticos
Los recursos estáticos (CSS, JS, imágenes, documentos PDF) pueden servirse desde CDN (CloudFlare, AWS CloudFront) para reducir carga del servidor y mejorar latencia global. Los assets se suben periódicamente al CDN y se sirven desde edge locations cercanas al usuario.
Historial de cambios
- Versión inicial del documento de arquitectura
- Descripción completa de la arquitectura de tres capas
- Documentación del motor de base de datos JSON
- Explicación del hash partitioning
- Componentes core del sistema
- Sistema de multi-tenancy
- Patrones de diseño utilizados
- Estrategias de rendimiento y optimización
- Seguridad en profundidad
- Backup y escalabilidad