Manuel Osvaldo Montes Blancarte

// código · ia · web

Mi trabajo

Orme — página de inicio 00 Captura aquí

Web · Frontend

Orme

Mi primera página web, desarrollada en equipo durante la universidad. Un proyecto puramente de frontend donde di mis primeros pasos maquetando con HTML y CSS, y diseñando la identidad visual —botones, gráficos y el logo— en Photoshop.

HTMLCSSJavaScriptPhotoshopEquipo
Ver proyecto
Orme — página de inicio
Orme — página de contacto
Tipo
Frontendsin backend
Equipo
En equipoproyecto universitario
Páginas
5inicio, nosotros, servicios, blog, contacto
Diseño
Photoshoplogo, botones y gráficos
Cómo se hizo

Fue mi primer proyecto web real, hecho en equipo durante la universidad. Maquetamos la estructura con HTML y CSS, y toda la identidad visual —los botones, los gráficos y el logo— la diseñamos en Photoshop antes de llevarla al código. Repartimos el trabajo entre el equipo: cada quien tomó secciones y las fuimos integrando.

Cómo funciona

Es un sitio de puro frontend: la navegación entre secciones, el diseño responsivo y los elementos visuales corren enteramente en el navegador, sin backend. En la portada corre un video de fondo y los botones y gráficos exportados desde Photoshop se integraron como parte del diseño de cada página.

Lo que me atoró

Al ser mi primer proyecto en equipo, lo más complejo no fue el código en sí, sino coordinar el trabajo de varias personas y mantener un diseño coherente entre las partes que hacía cada quien: que los estilos, espaciados y componentes se vieran como un solo sitio y no como piezas sueltas.

Cómo lo resolví

Acordamos una base visual común desde Photoshop y nos apoyamos en ella como guía para todo el equipo. Fue donde aprendí a dividir un proyecto, integrar el trabajo de otros y cuidar la consistencia de un diseño — bases que sigo aplicando hoy en cada página que hago.

Equipo
Código destacado

El retoEn computadora el logo y el menú van en una sola fila, pero en un celular no caben: los enlaces se amontonaban y el texto se salía de la pantalla.

Cómo lo resolvíCon una media query: debajo de 768 px la barra pasa de fila a columna con flex-direction: column, y cada enlace se vuelve un botón ancho (80 %) fácil de tocar con el dedo. El eslogan y el formulario de contacto también se reacomodan para que todo quepa sin hacer zoom.

El retoEl video de la portada tenía que llenar todo el espacio en cualquier pantalla sin deformarse, y sin que su brillo compitiera con el resto del diseño.

Cómo lo resolvíobject-fit: cover hace que el video se recorte en lugar de estirarse, y una capa oscura encima (::after) baja su brillo. Esa capa lleva pointer-events: none para que no bloquee los clics al video.

Pantalla de acceso de Nexus 01 Captura aquí

Bases de datos · Proyecto integrador

Nexus · crime_bd

Sistema de control criminal sobre una base de datos MySQL: casos, detectives, criminales, evidencias y bitácora con triggers. Proyecto en equipo de Bases de Datos (UdeG): la base de datos y las consultas, a mano; la interfaz, con IA (Claude).

PróximamenteUna app que muestra paso a paso cómo se crea una base de datos.

SQLMySQLNode.jsExpressJWTEquipo
Ver en vivo
Pantalla de acceso de NexusPantalla de acceso de Nexus
Diagrama entidad-relación de crime_bdDiagrama entidad-relación de crime_bd
CREATE TABLE: 12 tablas y 5 triggers, sin erroresCREATE TABLE: 12 tablas y 5 triggers, sin errores
INSERT: catálogos y datos de pruebaINSERT: catálogos y datos de prueba
ALTER, DELETE y DROP controladosALTER, DELETE y DROP controlados
SELECT de la relación muchos a muchosSELECT de la relación muchos a muchos
Motor
MySQLdiseñada en MySQL Workbench
Tablas
12catálogos, pivotes y auditoría
Muchos a muchos
2involucrados · casos_etiquetas
Triggers
5bitácora automática en JSON
Datos de prueba
58 personas25 detectives · 12 casos · 15 evidencias
Hecho a mano
Todo el SQLla interfaz, con IA (Claude)
Lo importante aquí Es un proyecto de la materia de Bases de Datos: lo que se evaluaba era el diseño, la integridad de los datos y las consultas. Todo eso lo escribimos a mano en SQL. La parte visual la generamos con IA para poder enseñar la base de datos funcionando, así que en este caso de estudio el código que muestro es el de la base de datos y el de las consultas.
El proyecto

Diseñar e implementar crime_bd, una base de datos relacional para una corporación policial: registrar casos y su estado, asignar al detective responsable, llevar el expediente de cada persona involucrada (una misma persona puede ser testigo en un caso y sospechosa en otro), guardar evidencias y notas, y que cualquier cambio quede registrado.

Cómo se diseñó
  • Diagrama primero. Antes de escribir SQL hicimos el diagrama entidad-relación con todas las tablas, llaves y cardinalidades.
  • Modelo relacional sin repetir datos. Las tablas se unen por IDs: si el nivel de peligrosidad de alguien cambia, se edita en un solo lugar.
  • Catálogos. Rangos (con un nivel del 1 al 10 para ordenar la jerarquía), departamentos, estados del caso y etiquetas.
  • Dos relaciones muchos a muchos con tablas puente: involucrados (casos ⇄ personas, con su rol) y casos_etiquetas.
  • Tipos de dato con razón: DECIMAL para dinero, CHAR para documentos, ENUM para listas cerradas, DATETIME para la hora exacta y JSON para la bitácora.
  • Auditoría con triggers. Cinco triggers guardan cada INSERT, UPDATE o DELETE en casos, detectives y criminales, con los datos de antes y después.
Lo que nos atoró
  • Borrar tablas. DROP TABLE fallaba porque otras tablas dependían de ella por llaves foráneas.
  • El AUTO_INCREMENT. MySQL no nos dejaba ponérselo a id_nota con ALTER TABLE.
  • IDs manuales. Algunas tablas no autoincrementan, y la app tenía que calcular el siguiente ID sin chocar.
  • Operaciones a medias. Crear o borrar un caso toca varias tablas; si algo falla en medio, quedan datos huérfanos.
Cómo lo resolvimos
  • ON DELETE CASCADE para borrar en cadena, y en la app, borrar en orden: de las tablas hijas a la tabla padre.
  • Era cuestión de orden: primero la llave primaria, luego el AUTO_INCREMENT.
  • El siguiente ID y los INSERT van dentro de una transacción, con ROLLBACK si algo falla.
  • Consultas siempre parametrizadas para evitar inyección SQL.
De la base de datos a la web

Para verla en acción, la conectamos a una API en Node.js con Express: login con JWT, rutas para casos, detectives, criminales, evidencias y notas, y un tablero con métricas. La interfaz (login, tablero, expedientes y modo día/noche) la generamos con IA. Todo usa datos de prueba inventados.

Lo que sigue

Una app que muestre, paso a paso, cómo se crea una base de datos: del problema al diagrama, a las tablas, a las llaves y a las consultas. Se añadirá aquí mismo, junto a Nexus.

Equipo

Universidad de Guadalajara · CUTlaquepaque · Bases de Datos

Código destacado

El retoEra la base de todo el proyecto: definir qué se guarda, con qué tipo de dato y cómo se conectan las tablas, sin permitir datos duplicados ni registros que apunten a nada.

Cómo lo resolvíSeparamos los datos en tablas por tema (personas, detectives, casos…) y las unimos solo por sus IDs. Así el nombre de alguien se guarda una sola vez, aunque aparezca en muchos casos. Cada tipo de dato tiene su razón: CHAR para documentos de longitud fija, DECIMAL para dinero, ENUM para listas cerradas. La tabla involucrados usa una llave primaria compuesta, y las llaves foráneas van al final para crear primero todas las tablas y luego conectarlas.

El retoUn caso puede tener varias etiquetas (fraude, cibercrimen, violencia…) y una etiqueta se repite en muchos casos. Eso no cabe en una sola columna.

Cómo lo resolvíUna tabla pivote, casos_etiquetas, que solo guarda pares de IDs. Con dos JOIN se reconstruye la relación completa: 21 filas, cada caso con cada una de sus etiquetas. La misma idea se usa en involucrados entre casos y personas. Además, la tabla auditoria se llena sola con 5 triggers que guardan qué tabla cambió, la acción (INSERT, UPDATE o DELETE) y los datos de antes y después en JSON.

El retoLas evidencias son de tipos muy distintos (fotos, videos, documentos, objetos físicos) y cada una tiene que quedar ligada a su caso y al detective que la registró, con la hora exacta.

Cómo lo resolvíUn ENUM limita el tipo a una lista válida, ruta_archivo acepta NULL para evidencia física sin archivo y DATETIME con DEFAULT CURRENT_TIMESTAMP pone la fecha y hora solo, sin depender de quien captura. Las dos llaves foráneas impiden registrar una evidencia de un caso o detective que no existe.

El retoLa materia pedía modificar una base de datos que ya tenía datos: agregar columnas, borrar registros y tablas. Aquí nos atoramos dos veces: MySQL no dejaba borrar tablas por las llaves foráneas, ni poner AUTO_INCREMENT a una columna.

Cómo lo resolvíEl nivel de peligrosidad entró con ALTER TABLE y un valor por defecto, sin tocar los 10 criminales existentes. El DELETE va con las tres columnas de la llave para borrar exactamente una fila. Lo del AUTO_INCREMENT era el orden: una columna solo puede autoincrementar si antes es llave primaria. Y para poder borrar sin dejar datos sueltos usamos ON DELETE CASCADE.

El retoLa pantalla de casos necesita, en una sola fila, el caso, su estado, el detective a cargo con su departamento y todas sus etiquetas. Eso está repartido en siete tablas.

Cómo lo resolvíCinco JOIN para los datos obligatorios y dos LEFT JOIN para las etiquetas, así no desaparecen los casos que aún no tienen ninguna. GROUP_CONCAT junta las etiquetas de cada caso en una sola celda y GROUP BY evita filas repetidas. Los filtros del buscador viajan como parámetros (?), nunca pegados al texto del SQL: así no hay inyección SQL.

El retoEl tablero de inicio muestra cuántos casos hay en total, abiertos, en investigación y cerrados. Hacer una consulta por cada número era lento y repetitivo.

Cómo lo resolvíEn MySQL una comparación como id_estado = 1 vale 1 o 0, así que SUM() la convierte en un contador: los cuatro números salen de una sola lectura de la tabla. La segunda consulta usa la tabla puente para armar el expediente de un criminal: todos los casos donde aparece y con qué papel.

El retoCrear o borrar un caso toca varias tablas. Si algo fallaba a la mitad, quedaban casos sin etiquetas, o etiquetas y evidencias de un caso que ya no existe.

Cómo lo resolvíTodo va dentro de una transacción: o se guardan el caso y sus etiquetas juntos, o con ROLLBACK no se guarda nada. Para borrar se respeta el orden de las llaves foráneas, de los hijos al padre, así nunca quedan registros huérfanos.

Primer prototipo de Hi: LSTM detectando puntos de mano y cara 02 Captura aquí

Inteligencia artificial · Visión por computadora

Hi · Deletreo en LSM

Software que reconoce el abecedario de la Lengua de Señas Mexicana con la cámara y lo escribe en tiempo real. Lo desarrollamos en equipo en la UdeG; yo diseñé la IA. Entrenamos nuestros propios modelos con más de 55 mil imágenes y salimos en la Gaceta UdeG.

La demo de aquí es una versión ligera: solo una parte del trabajo, adaptada para correr en el navegador.

PythonPyTorchMediaPipeTransformersONNXEquipo
Probar demo
Primer prototipo: LSTM con puntos de mano y caraPrimer prototipo: LSTM con puntos de mano y cara
La demo web: deletreando La demo web: deletreando "ABCDE" con la cámara
Mano quieta: el modelo reconoce la A con 77 %Mano quieta: el modelo reconoce la A con 77 %
La identidad del proyecto: La identidad del proyecto: "hi!"
Versión de escritorio en Python + OpenCVVersión de escritorio en Python + OpenCV
Plataforma completa: 55,974 imágenes y 4 modelosPlataforma completa: 55,974 imágenes y 4 modelos
Panel de entrenamiento: EfficientNet o MLPPanel de entrenamiento: EfficientNet o MLP
Inferencia en vivo con el Transformer de movimientoInferencia en vivo con el Transformer de movimiento
Inferencia con el modelo combinado por FedAvgInferencia con el modelo combinado por FedAvg
El límite: 94 % de RAM y 90 % de GPU entrenandoEl límite: 94 % de RAM y 90 % de GPU entrenando
El equipo en IntelEl equipo en Intel
Dataset
55,974imágenes grabadas por el equipo
Precisión
80 – 90 %según el modelo y la luz
Arquitecturas
5 probadasLSTM · MLP · MobileNet · EfficientNet · Transformer
Demo web
~4 MBversión ligera: 3 modelos ONNX en el navegador
Equipo
5 + asesoraIng. Informática · CUTlaquepaque
Prensa
Gaceta UdeGabril 2026
Sobre la demo Lo que pruebas aquí es una versión ligera, no el proyecto completo. Es solo el reconocimiento del abecedario, con tres modelos pequeños (~4 MB) que corren en tu navegador. El sistema original es mucho más grande: plataforma para grabar el dataset, entrenamiento con gráficas en vivo, cinco arquitecturas de IA, servidor en Python con GPU, login por integrante y aprendizaje federado. Ese no se puede dejar en línea por lo pesado que es (ver "Lo que nos atoró"), así que aquí abajo te enseño fotos y código de esa versión.
El problema

Las personas sordas muchas veces dependen de un intérprete para comunicarse con quien no sabe Lengua de Señas Mexicana, y pierden autonomía. Quisimos empezar a cerrar esa brecha con algo que cualquiera tenga a la mano: la cámara de una computadora o un celular, entendiendo el deletreo del abecedario y convirtiéndolo en texto y voz.

Cómo se hizo

Lo construimos por etapas, y cada una nos enseñó algo:

  • Prototipo con LSTM. Detectando puntos de manos, cara y torso con MediaPipe, y un modelo LSTM que leía secuencias.
  • App de escritorio. En Python con OpenCV: cámara, puntos de la mano, la letra con su porcentaje y el texto deletreado.
  • Dataset propio. Grabamos videos de cada letra y extrajimos 55,974 imágenes y secuencias de movimiento.
  • Plataforma completa. Frontend en Next.js para capturar datos, entrenar con gráficas en tiempo real e inferir; backend en Node.js con login JWT y WebSockets; y un servidor de IA en Python (FastAPI + PyTorch).
  • Varios modelos. Probamos CNN sobre imágenes (MobileNet, EfficientNet), un MLP sobre los 21 puntos de la mano y un Transformer de movimiento para las letras que se trazan en el aire.
  • Aprendizaje federado (FedAvg). Cada integrante entrenaba con sus propios datos y luego promediábamos los pesos en un modelo global.
  • Demo web. Al final la convertí en una versión ligera que corre en el navegador: solo el reconocimiento, sin la plataforma, el entrenamiento ni el servidor de IA.
Cómo funciona la IA

MediaPipe encuentra 21 puntos de la mano en cada cuadro y los convierte en 63 números, relativos a la muñeca y normalizados. Esos números pasan por tres modelos: un MLP para las letras quietas, un Transformer que mira los últimos 20 cuadros (todas las letras) y otro Transformer especializado en las 8 letras con movimiento (J, Z, RR, LL, X, N, K, Q). El sistema mide cuánto se mueve la mano y mezcla sus respuestas; la letra se escribe cuando la sostienes un momento.

Lo que nos atoró
  • La memoria. Entrenar con decenas de miles de imágenes llenaba la computadora: 94 % de RAM (29.8 de 32 GB) y la GPU al 90 %. Ese límite es la razón por la que el proyecto está en pausa en esta etapa.
  • Demasiado pesado para publicarlo. La versión completa necesitaba el servidor de IA en Python encendido todo el tiempo.
  • Las letras con movimiento. Viendo una sola imagen, una J se confunde con una I: había que entender el trazo en el tiempo.
  • Luz y fondos. Con poca luz o fondos saturados el reconocimiento falla más.
  • Que la web "viera" igual que Python. Cualquier diferencia al preparar los datos y el modelo se equivoca.
  • Coordinar a cinco personas entrenando con sus propias grabaciones.
Cómo lo resolvimos
  • De imágenes a puntos: en lugar de mandarle la foto completa al modelo, solo le damos 63 números por cuadro. El MLP pesa 0.3 MB contra los 17.7 MB de EfficientNet, y su precisión es igual de buena.
  • A la web: exporté los modelos de PyTorch a ONNX y los cuanticé a 8 bits; corren con WebAssembly en el navegador, el servidor solo entrega archivos y el video nunca sale del dispositivo.
  • Movimiento: Transformers que leen secuencias de 20 cuadros, más una mezcla que da más peso a uno u otro modelo según cuánto se mueve la mano.
  • Estabilidad: suavizado entre cuadros, seguridad mínima y tiempo de sostén para que no se escriba basura.
  • Mismos datos: repliqué en JavaScript, paso por paso, el preprocesamiento de Python.
  • Equipo: FedAvg para combinar en un solo modelo lo que entrenaba cada quien.
Lo que sigue

La meta es que la comunicación vaya en los dos sentidos: de señas a texto y voz, y de voz a texto y a un avatar 3D que haga las señas, para que personas sordas y oyentes se entiendan sin depender de un intérprete.

Equipo
  • Osvaldo Blancarte · Blancarte-7655 — yo: diseño de la IA, entrenamiento y sistema
  • Daniel Serrano · DannNS20 — sistema (backend y frontend) y entrenamiento
  • Alberto López · BetoGDL51 — entrenamiento
  • Kevin Pérez · kevinperez357 — entrenamiento
  • Karim Ramírez · Markozrm — entrenamiento
  • Mtra. Rocío Padilla — asesora: imagen y difusión del proyecto
Código destacado

Primero el sistema original (dataset, entrenamiento en GPU, FedAvg, backend y frontend) y al final la versión ligera que corre aquí.

El retoversión originalNo existía un dataset del abecedario de LSM como lo necesitábamos. Teníamos que grabarlo nosotros mismos: miles de imágenes por letra, de cinco personas distintas, sin que fuera un martirio.

Cómo lo resolvíUna pantalla de captura en Next.js: eliges la letra, haces la seña y la app toma ~6 fotos por segundo, ya en espejo y recortadas, con un contador hacia la meta de 200 por letra. Así juntamos 55,974 imágenes; después un proceso en Python les extraía los puntos de la mano y las secuencias de movimiento.

El retoversión originalLas letras que se trazan en el aire (J, Z, RR, LL, X, Ñ, K, Q) no caben en una foto. El modelo tenía que leer una secuencia y entender el orden de los movimientos.

Cómo lo resolvíDiseñé un Transformer que recibe 20 cuadros seguidos de puntos de la mano. La codificación posicional le da el orden; la atención (8 cabezas) compara cada cuadro con los demás, así distingue una I quieta de una J que se curva. Con esta misma clase entrenamos el modelo de todas las letras y uno especializado en movimiento, que fue el que mejor reconoció las letras en movimiento.

El retoversión originalEntrenar con decenas de miles de imágenes en el procesador tardaba horas. Necesitábamos la tarjeta NVIDIA de la computadora y ver el avance en vivo, no esperar a ciegas a que terminara.

Cómo lo resolvíEn el panel elegías el modelo, el dataset y el dispositivo: GPU con CUDA (la tarjeta NVIDIA), CPU o automático. PyTorch movía el modelo y cada lote de datos a la GPU, que hace miles de multiplicaciones en paralelo. Cada época, Python mandaba una línea por Server-Sent Events y el frontend la convertía en puntos de la gráfica de pérdida y precisión, avisando cuando se guardaba un nuevo mejor modelo. También se podía reanudar un entrenamiento en lugar de empezar de cero.

El retoversión originalCinco personas podían lanzar entrenamientos desde la plataforma, pero la máquina ya andaba al límite de RAM y GPU. Además cada modelo tenía que quedar registrado a nombre de quien lo entrenó, para luego combinarlos con FedAvg.

Cómo lo resolvíEl backend en Node revisa el token, aplica un límite de 5 entrenamientos por hora y toma el nombre del integrante del token, no del formulario. Python entrenaba y mandaba el progreso (pérdida, precisión, época) en vivo por Server-Sent Events para las gráficas del panel; al final, FedAvg promediaba los pesos de cada quien en un modelo global.

El retoversión originalCada integrante entrenaba con sus propias grabaciones en su computadora. Teníamos cinco modelos distintos y queríamos uno solo que supiera lo de todos, sin juntar todos los videos en un mismo lugar.

Cómo lo resolvíUsamos aprendizaje federado (FedAvg). Cada modelo entrenado se guardaba como “contribución” con el nombre de quien lo hizo (sacado del token). Desde el panel, un admin lanzaba la fusión: Python cargaba los pesos de todas las contribuciones del mismo tipo, promediaba capa por capa y validaba el resultado. Así salió el modelo global (EfficientNet FedAvg), que aparece marcado como “FedAvg” al elegir modelo en la inferencia.

El retoversión originalEl frontend no debía hablar directo con el servidor de IA: había que poner en medio login, límites, registro de peticiones y una sola dirección pública para todo.

Cómo lo resolvíUn API en Express que concentra todo: CORS solo para nuestro frontend, JWT, rate limiting, multer para recibir videos y fotos, axios para reenviar a Python (FastAPI) y el relay de WebSockets. Si Python se cae, Node responde con un error claro (502) en lugar de dejar al frontend colgado.

El retoversión originalEn la versión completa, el frontend tenía que mandar los cuadros al servidor de IA en Python, pero sin exponer esa dirección ni dejarlo abierto a cualquiera.

Cómo lo resolvíUn backend en Node.js hacía de intermediario: login con JWT, límites de peticiones y un relay de WebSockets en tiempo real entre el navegador y el servidor Python (FastAPI + PyTorch). Ese sistema completo también tenía captura del dataset, entrenamiento con gráficas y aprendizaje federado (FedAvg): cada integrante entrenaba con sus datos y luego se promediaban los pesos en un modelo global.

El retoversión originalEn la plataforma completa la IA vivía en el servidor con la GPU. El navegador tenía que mandarle video sin parar y recibir la letra al instante, y además poder cambiar la velocidad o la seguridad sin cortar la conexión.

Cómo lo resolvíLa cámara se reduce a 320×240 y se manda como JPEG 10 veces por segundo por WebSocket; el servidor responde la letra, su seguridad, el top 3 y los puntos de la mano para dibujarlos. Los ajustes viajan como mensajes config por el mismo canal, y se podía elegir qué modelo usar, incluido el combinado por FedAvg.

El retoversión ligeraLa primera versión necesitaba un servidor Python con los modelos completos: pesada y cara de mantener en línea. Queríamos que cualquiera la probara desde su celular.

Cómo lo resolvíExporté los tres modelos de PyTorch a ONNX y los cuantizé a 8 bits: entre los tres pesan unos 4 MB. En el navegador corren con ONNX Runtime en WebAssembly, junto con MediaPipe. Resultado: el servidor solo entrega archivos, todo el cálculo pasa en el dispositivo y el video nunca sale de él.

El retoversión ligeraHacer que el navegador, sin servidor de IA, cargara tres redes neuronales y el detector de manos de Google, pidiera permiso de cámara y procesara cada cuadro sin trabarse.

Cómo lo resolvíONNX Runtime Web corre los modelos en WebAssembly y MediaPipe Hands encuentra la mano; todo se sirve desde nuestro propio servidor (carpeta vendor/), sin depender de CDNs. La cámara se pide con getUserMedia y, si la niegan, se explica cómo permitirla. El ciclo con requestAnimationFrame y una bandera busy procesa un cuadro a la vez, para que un celular lento no se sature.

El retoversión ligeraLa cámara da video, pero un modelo necesita números. Y tenían que ser exactamente los mismos números con los que entrenamos en Python, o el modelo se confunde.

Cómo lo resolvíCada cuadro se convierte en 63 números: la posición de los 21 puntos relativa a la muñeca y normalizada. Así da igual si la mano está cerca o lejos de la cámara, o en qué parte de la imagen. Esta función replica línea por línea la de Python, para que el modelo vea en el navegador lo mismo que vio al entrenar.

El retoversión ligeraHay letras que se hacen con la mano quieta y otras con movimiento (J, Z, RR, LL…). Un solo modelo no hacía bien las dos cosas.

Cómo lo resolvíDiseñé un sistema de tres modelos: un MLP rápido para las 21 letras quietas, un Transformer que mira los últimos 20 cuadros para las 29 letras, y otro Transformer especializado en las 8 letras con movimiento. El código mide cuánto se mueve la mano y mezcla sus respuestas: con la mano quieta manda el MLP; en cuanto se mueve, toman el control los Transformers.

El retoversión ligeraEl modelo cambia de opinión varias veces por segundo. Si se escribiera cada respuesta, saldría basura en pantalla.

Cómo lo resolvíPrimero se suavizan las probabilidades entre cuadros. Luego una letra solo se escribe si supera la seguridad mínima (60 %) y se sostiene 0.8 s; el anillo alrededor de la letra se va llenando para que la persona vea el avance. Al retirar la mano se agrega un espacio, y el texto se puede leer en voz alta.

Sistema de rentas — panel general 05 Captura aquí

Web · Full stack

Sistema de registro de rentas

Aplicación web que usa mi familia para administrar las rentas de sus locales: ingresos, gastos, repartos y deudas por mes, con gráficas, reportes en Excel y PDF, roles de usuario y bitácora de auditoría. Corre en producción en mi propio servidor.

Node.jsMySQLJavaScriptNginxSeguridad
Ver demo
Panel general con gráficas
Lista de movimientos del mes
Reportes con exportación a Excel y PDF
Bitácora de auditoría encadenada
Panel en tema oscuro
Pantalla de entrada
Estado
En producciónla usa mi familia cada mes
Historial
682 movimientosde libreta → Excel → app
Acceso
2 rolesadministrador y espectador
Entrada
Huella / Face IDpasskeys, sin guardar biometría
Reportes
Excel y PDFhechos sin librerías
Dependencias
MínimasNode puro, sin frameworks
El problema

Mi familia llevaba las rentas de sus locales escritas a mano en una libreta: quién pagó, cuánto se gastó en luz, agua o predial, cuánto se repartió y quién le debe a quién. Necesitaban un solo lugar, accesible desde el celular, donde todos vieran lo mismo pero solo algunos pudieran modificar.

Cómo se hizo

Backend en Node.js puro, sin frameworks: escribí mi propio mini-marco para sesiones, cookies, formularios y archivos estáticos, con MySQL como base de datos. Vive en mi servidor Hetzner detrás de Nginx y Cloudflare, con PM2 manteniéndolo encendido y GitHub Actions desplegando cada cambio automáticamente.

Cómo funciona

Se registran ingresos, gastos, repartos y deudas; el panel arma las gráficas y saldos solo. Hay roles (administrador y espectador), registro con código de invitación y aprobación, entrada con huella o Face ID, reportes en Excel y PDF, modo oscuro, opciones de accesibilidad y se instala en el celular como app.

Lo que me atoró

Fueron varios retos a la vez:

  • Pasar todo el historial. Todo estaba escrito a mano en una libreta. Primero lo pasé a Excel, renglón por renglón, y luego esos años de registros (682 movimientos hasta julio de 2026) tenían que entrar a la base de datos sin perder ni un peso. Desde agosto la familia ya captura mes a mes directo en la app.
  • Acomodar lo visual. Que tanta información (saldos, gráficas, deudas, repartos) se entendiera de un vistazo, en computadora y en celular.
  • Exportar a PDF y Excel con el formato que la familia ya conocía, sin depender de librerías pesadas.
  • Seguridad y roles. Son datos de dinero: quién puede ver, quién puede modificar y quién puede dar permisos de administrador.
  • Seguridad de la página contra ataques comunes y una forma de entrar con datos biométricos (huella o Face ID).
Cómo lo resolví
  • Migración: de la libreta a Excel a mano; ahí revisé y corregí cada mes. Luego generé el SQL y lo importé en una sola transacción con respaldo previo, comparando al final el total contra el Excel.
  • Visual: un panel con tarjetas de saldo, gráfica por año, ingresos vs. gastos por mes y una dona de "¿en qué se fue el dinero?", con diseño adaptable.
  • Exportar: escribí el generador de Excel (.xlsx es un ZIP de XML) y el de PDF a mano con Node puro.
  • Roles: administrador y espectador, máximo 4 admins, nunca dejar el sistema sin admin, y un segundo código secreto para dar o quitar ese poder.
  • Seguridad: contraseñas con scrypt, cabeceras de seguridad (CSP), cookies HttpOnly/SameSite, CSRF, freno de fuerza bruta, bitácora encadenada con hashes y passkeys (WebAuthn) para entrar con huella sin que la huella salga nunca del teléfono.

La demo que ves aquí usa una base de datos falsa en memoria: el mismo código, pero con datos inventados.

La huella aquí y en UniAccess

Las dos apps usan passkeys (WebAuthn) y en ninguna la huella sale del teléfono, pero con objetivos distintos:

  • Aquí es para comodidad: entrar a la cuenta sin escribir contraseña. Es opcional (la contraseña sigue funcionando), cada quien puede tener varias passkeys (celular, laptop) y la passkey misma dice quién eres, sin escribir usuario.
  • En UniAccess es una prueba: firma cada entrada y salida del campus. Es obligatoria en cada registro, solo se permite un teléfono por persona y se liga a la sesión ya iniciada, para que nadie registre el acceso de otro.
Código destacado

El retoLos registros venían de una libreta escrita a mano que primero pasé a Excel. Después había que llevar esos años de datos del Excel a MySQL sin perder nada y sin dejar la base a medias si algo fallaba.

Cómo lo resolvíPrimero un respaldo completo con mysqldump. Luego todo dentro de una transacción: si una sola fila fallaba, no se guardaba nada y la base quedaba como antes. Al final, una consulta de verificación compara el número de movimientos y el saldo contra el Excel, peso por peso.

El retoGenerar reportes en Excel con colores, totales y formato, sin instalar librerías pesadas en el servidor.

Cómo lo resolvíDescubrí que un archivo .xlsx es un ZIP lleno de XML. Así que genero el XML de cada hoja (celdas, estilos, columnas combinadas) y lo empaqueto con zlib, que ya viene en Node, calculando yo mismo el CRC32 que exige el formato ZIP. El PDF ejecutivo lo hice igual: escribiendo directo las instrucciones del formato PDF. Cero dependencias.

El retoQue solo los administradores modifiquen datos, que nunca se quede el sistema sin administrador y que no cualquiera pueda volverse admin.

Cómo lo resolvíDar o quitar el rol de administrador pide un segundo código secreto, distinto al de invitación, con su propio freno de intentos. Hay un límite de 4 administradores, nunca se puede quitar al último, se cierran las sesiones del usuario para que el cambio aplique al instante y todo queda en la bitácora.

El retoPermitir entrar con huella o Face ID sin guardar nunca datos biométricos en el servidor.

Cómo lo resolvíUsé passkeys (WebAuthn): la huella se revisa dentro del propio teléfono y este firma un reto con una llave privada que nunca sale del dispositivo. El servidor solo guarda la llave pública y verifica la firma, el dominio y un contador que detecta copias clonadas. Si un usuario está suspendido, la passkey ya no le sirve.

El retoProteger la página contra ataques comunes del navegador: scripts inyectados, que la metan en un iframe para engañar, o que lean datos de otros sitios.

Cómo lo resolvíCada respuesta lleva una Content-Security-Policy que solo permite cargar código e imágenes del propio sitio, prohíbe que otra página la muestre dentro (clickjacking) y fuerza HTTPS con HSTS. Junto con cookies HttpOnly y SameSite=Strict, tokens CSRF y contraseñas con scrypt, cubre los ataques más comunes.

El retoGuardar todo lo que pasa en el sistema (quién entró, qué borró, qué cambió) de forma que, si alguien edita o borra una fila directo en la base de datos, se detecte.

Cómo lo resolvíCada registro guarda el hash SHA-256 de sus datos más el hash del registro anterior, formando una cadena. Si se altera una sola fila, su hash ya no coincide y toda la cadena desde ahí se rompe: la pantalla de auditoría lo marca en rojo. Los registros pasan por una cola (cola.then) para que dos eventos al mismo tiempo no partan la cadena en dos, y la hora se guarda en milisegundos para que el hash salga idéntico al volver a verificarlo.

El retoBloquear a quien intenta adivinar contraseñas, pero sin que un solo atacante deje fuera de la app a toda la familia.

Cómo lo resolví5 intentos fallidos en 15 minutos y esa combinación queda bloqueada. La llave combina la IP real (la que manda Cloudflare) con el usuario: así, si alguien ataca una cuenta, solo se bloquea ese intento y el resto de la familia sigue entrando normal. La respuesta dice cuántos minutos faltan.

El retoMostrar el sistema en mi portafolio sin exponer los datos reales de la familia.

Cómo lo resolvíToda la app habla con la base de datos a través de una sola función q(). En modo demo esa función se cambia por una versión en memoria que responde a las mismas consultas con datos inventados (locales, montos y notas ficticios generados a partir de la fecha de hoy). Corre como un proceso aparte, sin tocar MySQL, y se reinicia sola cada 6 horas para borrar lo que muevan los visitantes.

Portada de UniAccess con el portal de acceso y el portal institucional 05 Captura aquí

Web · Full stack · Seguridad

UniAccess

Control de entradas y salidas del campus desde el celular de cada persona, sin lectores ni torniquetes: la huella o el rostro del teléfono confirman quién registra. Dos portales, tablero en vivo, invitados con QR y reportes en Excel. Lo programamos Daniel y yo.

ReactTypeScriptNode.jsSQLiteWebAuthn
Ver en vivo
Portada: portal de acceso y portal institucionalPortada: un portal para cada público
Tablero con gráficas de entradas y salidasTablero: entradas y salidas por día y por acceso
Portada en modo oscuroModo oscuro y opciones de lectura
Hardware
Ningunoel celular es la credencial
Identidad
WebAuthnhuella o rostro, nunca sale del teléfono
Portales
2sesiones y código separados
Barreras
4credencial · rol · red · geocerca
Sin librerías
QR y Excelhechos a mano
Calidad
Pruebas + ADR4 suites · 5 decisiones documentadas
El problema

En horas pico los accesos del centro universitario se llenan porque el registro de entrada y salida es manual y lento, y vigilancia no tiene un registro confiable de quién está dentro. Lo difícil: el campus no tiene lectores de credencial ni torniquetes, y no había presupuesto para comprarlos.

La solución

El celular de cada persona funciona como su credencial. Al llegar, registra su entrada y confirma con la huella o el rostro de su propio teléfono (WebAuthn): el servidor solo recibe una firma, nunca la biometría. Opcionalmente se valida que esté junto al acceso (geocerca) y conectado a la red del campus.

Qué hace
  • Portal de acceso (celular): entrada y salida con biometría, historial, horario, uso de equipos de laboratorio por QR, invitados con QR y avisos.
  • Portal institucional: tablero en vivo de quién está dentro, gráficas de afluencia, alertas, registro manual de respaldo, pases de invitado, laboratorios, lista de emergencia y reportes en Excel de 10 hojas.
  • Para todos: aviso de privacidad, manual interactivo, modo oscuro, texto grande y tipografía para baja visión.
Lo que nos atoró
  • Probar quién registra sin hardware. Una contraseña se puede prestar y un QR impreso se puede fotografiar.
  • Un hueco en la geocerca. La primera versión sumaba la precisión que reporta el teléfono al radio: mandando una precisión de 100,000 m se podía registrar desde cualquier lugar.
  • Que un alumno nunca viera lo de vigilancia. Con un solo portal, un olvido en una validación de rol exponía a toda la comunidad.
  • QR, Excel y avisos sin llenar el proyecto de librerías pesadas.
Cómo lo resolvimos
  • Passkeys con verificación del usuario obligatoria, desafío de un solo uso y un solo dispositivo por persona, para que nadie vincule su huella a la cuenta de otro.
  • Tope de ±100 m a la precisión del GPS, con una prueba automatizada que reproduce el ataque; y la red del campus como segunda barrera.
  • Dos portales con cookies, middleware y paquetes de código distintos: el código institucional ni siquiera se descarga en el celular de un alumno.
  • QR propio (Reed-Solomon incluido) y Excel propio (un ZIP con XML armado con zlib), sin dependencias nuevas.
  • Cada decisión quedó escrita como ADR, con su deuda técnica y casos de prueba.
Cómo funciona la biometría

Usa WebAuthn (passkeys), el mismo estándar con el que los bancos y Google permiten entrar con huella. La huella o el rostro nunca salen del teléfono: el servidor solo recibe una firma.

  • 1. Vincular (una sola vez). Desde su perfil, la persona toca "vincular biometría". El servidor manda un reto; el teléfono pide la huella o el rostro y crea un par de llaves: la privada se queda guardada en el chip seguro del teléfono y la pública se guarda en el servidor. Se permite un solo teléfono por persona y se avisa por correo.
  • 2. Registrar entrada o salida. Al tocar "Entrada", el servidor genera un reto nuevo que caduca en 5 minutos y solo sirve una vez. El teléfono vuelve a pedir la huella o el rostro (obligatorio, no basta con tener el teléfono desbloqueado) y firma el reto con la llave privada.
  • 3. Verificar. El servidor comprueba la firma con la llave pública, que venga del dominio correcto y que el contador del dispositivo haya avanzado (así no se puede reutilizar una firma vieja). Solo entonces guarda el registro.
Diferencia con la biometría de Rentas

Las dos usan passkeys, pero para cosas distintas:

  • Para qué: en Rentas la huella sirve para entrar a la cuenta sin escribir contraseña. Aquí sirve para firmar cada entrada y salida: es la prueba de que la persona registró su propio acceso.
  • Qué tan estricta: en Rentas es opcional (la contraseña sigue funcionando) y la verificación del usuario es "preferida". Aquí es obligatoria en cada registro.
  • Dispositivos: en Rentas cada quien puede tener varias passkeys (celular, laptop) con nombre. Aquí hay una sola, del propio teléfono, y cambiarla la autoriza la administración, para que nadie registre por otro.
  • Quién es: en Rentas la passkey dice quién eres (no escribes usuario). Aquí ya iniciaste sesión: el servidor solo acepta las llaves ligadas a tu cuenta.
Equipo

Universidad de Guadalajara · CUTlaquepaque · Seminario de Integración: Desarrollo

Código destacado

El retoAntes de guardar una entrada hay que revisar muchas cosas, y si algo falla, la persona debe saber exactamente qué hacer y vigilancia debe enterarse.

Cómo lo resolvimosUna sola función con las barreras en orden, de la más barata a la más cara: credencial, rol, red, ubicación y al final la biometría. Cada rechazo lanza un error con un mensaje claro para la persona y una alerta para vigilancia (con una clave para no repetirla el mismo día). Una salida sin entrada se guarda como incidencia.

El retoConfirmar que quien registra es el dueño de la cuenta, sin guardar nunca su huella ni su rostro.

Cómo lo resolvimosCon WebAuthn: el teléfono firma un desafío con una llave privada que nunca sale de él, y el servidor comprueba la firma con la llave pública guardada. El contador evita que se reutilice una firma vieja. Además solo se permite un dispositivo por persona; cambiarlo lo autoriza la administración.

El retoLa ubicación la manda el propio teléfono. La primera versión aceptaba cualquier precisión: enviando accuracy: 100000 se podía registrar una entrada desde 100 km.

Cómo lo resolvimosUn tope de ±100 m: un GPS normal da entre 5 y 20 m, así que una lectura peor se rechaza y se pide activar la ubicación precisa. Escribimos una prueba automatizada que reproduce el ataque y comprueba que ya no pasa. Como la ubicación se puede falsificar con una app, sumamos la red del campus como segunda barrera.

El retoQue el registro solo funcione conectado al WiFi del campus, y que el portal institucional abra solo desde las computadoras de la caseta, sin que la administración se pueda dejar fuera a sí misma.

Cómo lo resolvimosRangos de IP (CIDR) configurables desde el portal, revisados con BlockList, que ya viene en Node. Al guardar, si la nueva lista dejaría fuera la IP de quien la está guardando, se rechaza con un aviso.

El retoDos públicos con riesgos distintos: el alumno abre la app a diario para entrar; vigilancia ve datos de toda la comunidad.

Cómo lo resolvimosCookies distintas por portal, guardadas en la base solo como hash (si alguien roba la base, no puede usar las sesiones), con duración según el riesgo. Una sesión de alumno ni siquiera se reconoce en las rutas del portal institucional. Las bajas y los cambios de contraseña cierran todas las sesiones de esa cuenta.

El retoQue ni un error en el código pudiera guardar un registro imposible: una salida antes de la entrada, un estado inventado o un registro sin persona.

Cómo lo resolvimosSQLite integrado en Node (node:sqlite), sin instalar servidor de base de datos, con restricciones CHECK y llaves foráneas: la base rechaza sola lo que no tiene sentido. El esquema es SQL estándar, así que se puede migrar a PostgreSQL o SQL Server.

El retoLos invitados y los equipos del laboratorio necesitaban códigos QR. Lo normal es instalar una librería, pero cada dependencia es código ajeno que hay que auditar y mantener.

Cómo lo resolvimosUn generador de QR propio de unas 300 líneas: codificación, corrección de errores Reed-Solomon (lo que permite leer un QR aunque esté rayado), versiones 1 a 40 y elección de máscara. El mismo código lo usan el portal y las pruebas. Igual hicimos el Excel: un .xlsx es un ZIP con XML, armado con zlib de Node.

Portada de SIS, Sistema de Identidad Simulado 06 Captura aquí

Web · Frontend · En proceso

SIS

Sistema de identidad simulado: registro en línea, panel de operadores y credencial digital con elementos de seguridad, con datos ficticios. Lo programo yo solo; por ahora, la parte visual.

En procesoLo que sigue: generar códigos QR de verificación reales y adaptar el sistema a diferentes organismos.

ReactJavaScriptCSSAccesibilidad
Ver avance
Portada de SISPortada con la credencial de muestra
Credencial digital, anverso y reversoCredencial digital: 5 colores, anverso y reverso
Registro en 4 pasosRegistro en 4 pasos con detección de duplicados
Panel del operadorPanel del operador
Inicio de sesiónInicio de sesión: ciudadano u operativo
Estado
En procesofrontend con datos ficticios
Pantallas
6portal · registro · panel · credencial…
Credencial
5 coloressellos, microtexto y marca MUESTRA
Siguiente etapa
APINode + Express + PostgreSQL
En proceso Este proyecto aún no está terminado. Lo que ves hoy es la parte visual, con datos inventados; todavía no guarda nada. Es una simulación: no es un sistema oficial ni tiene relación con ninguna institución pública.
La idea

Simular de principio a fin cómo una persona se registra, un operador valida su registro y se emite una credencial digital que se puede verificar con un código QR.

El diseño
  • Estilo institucional: guinda y dorado de día, grafito y verde pino de noche, títulos en serif y filetes finos.
  • Credencial como objeto físico: se inclina siguiendo el cursor, tiene brillo holográfico, trama de seguridad, sellos y la marca MUESTRA.
  • Registro en 4 pasos con numeración maya y aviso de posibles duplicados.
  • Accesibilidad siempre visible: tamaño de texto, alto contraste y tema.
Lo que sigue
  • Generación de QR: hoy el QR de la credencial es decorativo; el siguiente paso es generarlo de verdad, ligado a la consulta de vigencia.
  • Diferentes organismos: que el mismo sistema se pueda adaptar a distintas instituciones.
  • Backend en Node.js con PostgreSQL; los datos ficticios de hoy ya describen lo que devolverá cada ruta de la API.
Equipo
Código destacado

El retoQue la credencial se sintiera como un objeto real en la pantalla, sin volverse molesta ni marear a nadie.

Cómo lo resolvíEl JavaScript solo calcula la posición y la guarda en variables CSS; la inclinación 3D y el brillo los dibuja el CSS, así va fluido. Con mouse sigue al cursor; en celular se inclina con el scroll. Si la persona pidió reducir movimiento en su sistema, no se mueve.

El retoLa persona elige el color de su credencial en una página, y la portada y la vista de la credencial debían mostrarlo igual, aunque recargue.

Cómo lo resolvíUn hook que guarda la elección en el navegador y avisa con un evento propio a todas las credenciales abiertas para que cambien a la vez. El color se pasa como variables CSS, así la misma tarjeta sirve para los 5 colores. Si el navegador está en modo privado, simplemente usa guinda.

Terminal con la conexión SSH a un servidor Ubuntu de ejemplo 07 Captura aquí

Infraestructura · Linux · Seguridad

Mi propio servidor

Así monto desde cero el servidor donde viven mis proyectos: un VPS en Hetzner con Ubuntu, acceso solo con llaves SSH, firewall, HTTPS automático, proxy con Nginx y despliegue con GitHub Actions. Paso a paso, con los comandos reales y datos de ejemplo.

LinuxNginxSSHCloudflareCI/CD
Entrada por SSH con llave, sin root ni contraseñaEntrada por SSH: con llave, sin root ni contraseña
Firewall activo y Fail2Ban bloqueando ataquesFirewall activo y Fail2Ban bloqueando ataques
Nginx, certificado HTTPS y cabeceras de seguridadNginx, HTTPS y cabeceras de seguridad
Apps con PM2 y despliegue automáticoApps siempre encendidas y despliegue automático
Proveedor
HetznerVPS con Ubuntu 24.04 LTS
Acceso
Solo llavesSSH ed25519, sin root ni contraseña
Puertos abiertos
322 · 80 · 443, todo lo demás cerrado
HTTPS
AutomáticoLet's Encrypt, se renueva solo
Despliegue
git pushGitHub Actions actualiza el servidor
En un servidor
Varios sitioscada dominio a su proyecto
Datos de ejemplo Los comandos y la configuración son los que uso, pero todos los datos son inventados: la IP es del rango reservado para documentación (203.0.113.x), el dominio es ejemplo.dev y el usuario es deploy. Así sirve como evidencia del proceso sin exponer mis servidores reales.
Cómo viaja una visita
Visitantenavegador CloudflareDNS · CDN · protección NginxHTTPS · cabeceras · proxy AppsNode con PM2 · sitios estáticos MySQLsolo desde el propio servidor
El paso a paso
  • 1. Crear el servidor. En Hetzner Cloud: Ubuntu LTS, ubicación y tamaño según el proyecto, y desde el inicio con mi llave SSH pública (nada de contraseñas).
  • 2. Primer acceso y usuario propio. Actualizar todo, crear un usuario con sudo y copiarle la llave. Root ya no se usa.
  • 3. Cerrar SSH. Sin entrada de root y sin contraseñas: solo llaves. Antes de cerrar la sesión, se prueba desde otra terminal que sí se puede entrar.
  • 4. Firewall. Todo cerrado por defecto; abiertos solo SSH, HTTP y HTTPS.
  • 5. Fail2Ban. Bloquea por un tiempo las IP que se equivocan muchas veces al entrar.
  • 6. Nginx. Un archivo por dominio: sitios estáticos directo y apps de Node detrás de un proxy, con cabeceras de seguridad.
  • 7. Dominio y Cloudflare. El DNS apunta al servidor y Cloudflare queda adelante con su caché y protección.
  • 8. HTTPS. Certbot pide el certificado, ajusta Nginx y lo renueva solo.
  • 9. Apps siempre encendidas. PM2 las reinicia si fallan y las levanta al reiniciar el servidor.
  • 10. Despliegue automático. Push a main → GitHub Actions entra por SSH con una llave solo para eso y actualiza.
  • 11. Mantenimiento. Actualizaciones de seguridad automáticas y respaldos diarios de la base de datos.
Capas de seguridad
  • Entrada: llaves SSH, sin root, sin contraseñas y Fail2Ban.
  • Red: firewall con solo 3 puertos; la base de datos no se expone a internet.
  • Tráfico: HTTPS obligatorio con HSTS y Cloudflare adelante.
  • Navegador: cabeceras CSP, nosniff, política de referencia y permisos; cada sitio decide quién puede mostrarlo dentro de otra página.
  • Secretos: contraseñas y llaves en archivos .env fuera del repo y en los secretos de GitHub, nunca en el código.
  • Mínimo privilegio: cada app corre con su propio usuario y su propia base de datos.
Lo que aprendí en el camino
  • Permitir SSH antes de prender el firewall, o te quedas fuera de tu propio servidor.
  • Probar siempre con nginx -t antes de recargar: un error de sintaxis tumba todos los sitios a la vez.
  • Guardar un respaldo de cada archivo antes de editarlo con un script.
  • La caché engaña: después de publicar, versionar los archivos (?v=2) y purgar Cloudflare.
Comandos y configuración

El retoEl servidor nace con el usuario root. Trabajar como root es peligroso: un error o un acceso robado tiene control total.

Cómo lo resolvíActualizo el sistema, creo un usuario propio con sudo y le copio la llave. A partir de aquí todo se hace con ese usuario y root deja de usarse.

El retoEn cuanto un servidor tiene IP pública, recibe cientos de intentos de entrar por SSH probando contraseñas.

Cómo lo resolvíDesactivo el acceso de root y las contraseñas: solo entra quien tiene la llave privada, y solo el usuario deploy. Siempre valido con sshd -t y pruebo desde otra ventana antes de cerrar la actual.

El retoCerrar todo lo que no se usa y frenar a quien intenta adivinar el acceso, sin dejarme fuera a mí mismo.

Cómo lo resolvíEl firewall niega todo por defecto y abre solo 3 puertos. Fail2Ban lee los registros de SSH y, si una IP falla 4 veces en 10 minutos, la bloquea una hora.

El retoUn solo servidor con varios dominios: cada uno debe llegar a su proyecto, y el navegador debe recibir reglas de seguridad.

Cómo lo resolvíUn archivo por dominio en sites-available, activado con un enlace en sites-enabled. Las apps de Node escuchan solo en 127.0.0.1 y Nginx les pasa el tráfico. Antes de recargar siempre: sudo nginx -t.

El retoQue todos los sitios usen HTTPS sin pagar certificados ni acordarme de renovarlos cada 90 días.

Cómo lo resolvíCertbot pide el certificado gratis a Let's Encrypt, modifica el sitio de Nginx para redirigir todo a HTTPS y deja programada la renovación automática.

El retoUna app de Node se apaga si cierro la terminal, si truena o si el servidor se reinicia.

Cómo lo resolvíPM2 mantiene cada app corriendo, la reinicia si falla o si usa demasiada memoria, guarda sus registros y la vuelve a levantar cuando el servidor se reinicia.

El retoSubir cambios a mano por SSH cada vez es lento y fácil de equivocarse.

Cómo lo resolvíCada git push a main dispara la acción: entra por SSH con una llave exclusiva guardada en los secretos de GitHub, trae la última versión y recarga la app sin tumbarla.

El retoUn servidor seguro hoy deja de estarlo si no se actualiza, y un error o un ataque puede borrar los datos.

Cómo lo resolvíLas actualizaciones de seguridad se instalan solas y cada madrugada se guarda un respaldo comprimido de la base de datos, conservando las últimas dos semanas. Además, los respaldos del propio proveedor guardan una copia del servidor completo.

¿Tienes un proyecto en mente?

[email protected]