MarvelousDB — Reporte técnico y de proceso

Demo pequeña desplegada en Fly.io. Sustenta una publicación sobre: (1) flujo de ideas con IA con metodología, (2) habilidades de ingeniería Backend y (3) divertirse.

Producción: https://marvelousdb.fly.dev/ Repo: marvelousdb (git local) Fecha: 07–09 de agosto de 2026


1. Resumen

MarvelousDB comenzó como un demo de 2014 (Express 3 + Orchestrate + Marvel API, ambos servicios muertos) y terminó siendo una base de datos de superhéroes 100% local y portable: 10,819 personajes (9,417 de dominio público del wiki Public Domain Super Heroes + 1,402 del seed de Marvel), 30,179 cómics, búsqueda full-text, filtros, un modo de batalla arcade y un panel de cobertura de imágenes — todo en un solo archivo SQLite de 269 MB servido por un API REST.


2. Proceso: flujo de ideas con IA (metodología)

Cada ronda siguió el mismo patrón: propuesta → pregunta de alcance → scope down → implementación → verificación con tests → decisión persistida (en ~/nef/decisions/, 14 documentos fechados).

#IdeaDecisión claveVerificación
1Quitar Orchestrate (DBaaS muerto)Reescribir como API REST separada + sitio que la consume12 tests
2BúsquedaFlexSearch en memoria → luego SQLite FTS5tests de búsqueda
3Datos sin Marvel APIImportar el wiki PDSH (MediaWiki Action API): 10,076 páginas → 9,417 personajes15 tests
4DiseñoReplicar el sitio de 2015 desde el Web Archive (el CDN de imágenes resultó vivo)render verificado
5VS BattleComparador estilo cameradecision + tiers de poder (el caso Cthulhu vs U.S. Agent calibra el sistema)test de 7 rondas
6Imágenes faltantesPipeline Fandom → Commons → Browser Run → SerpAPI, validado por magic bytes y poda de cachés rotos21 tests
7SQLite portablenode:sqlite + FTS5 trigram, nada en memoria: arranque 0.5s, 66 MB RSS (antes 7s y 500 MB)21 tests
8URLs compartibles/vs/cthulhu-vs-u-s-agent con suerte determinística por par (mismo ganador siempre)deepStrictEqual
9Lore sin IAPárrafo introductorio real de la bio, o plantilla pulp generada (estable por personaje)11 tests unitarios
10Narrativa arcade”CTHULHU used CULT FOLLOWING! SLAM!” + quotes de las bios + onomatopeyas + WINNER/LOSER27 tests API
11CoberturaPanel /coverage + botón de reporte de imagen mala que re-encola el cacheo11 tests e2e
12DeployFly.io con volumen persistente (se descubrió que el volumen tapa los scripts de data/ → require perezoso)smoke en producción

Aprendizajes metodológicos: (a) preguntar el alcance antes de escribir código (3 decisiones de arquitectura salieron de preguntas de 1-2 opciones); (b) calibrar contra un caso real (Cthulhu vs U.S. Agent destapó que la bio narrativa de Marvel generaba falsos tiers divinos → las secciones estructuradas mandan); (c) verificar con curl con y sin Referer (el bug del hotlinking de wikia solo aparecía en navegador); (d) cada decisión se persiste para no regenerarla.


3. Ingeniería Backend

Arquitectura

Browser ──► web.js (Express 4 + Handlebars, :5050)
                │  proxy /api/* (GET/POST/DELETE)
                ▼
           api.js (Express 4, :3001) ──► node:sqlite (FTS5 trigram, bm25)
                │
                ├─ data/marvelousdb.sqlite    (10,819 chars + 30,179 comics)
                ├─ data/img-cache.json        (id → imagen local, cache que manda)
                ├─ data/tmdb-films.json       (adaptaciones cinematográficas)
                └─ data/reported-images.json  (reportes de imagen mala)
  • Express 4 + Promesas nativas + fetch (Node ≥ 23.4, sin frameworks extra).
  • SQLite embebido (node:sqlite): esquema con columnas indexadas (universe, gender) + tablas FTS5 con tokenizer trigram (substring matching, case-insensitive) ordenadas por bm25. Nada en memoria: ~66 MB RSS.
  • Importadores (todos con API reales, incremental y reanudable):
    • data/import-pdsh.js — MediaWiki Action API (allpages + revisions + pageimages, formatversion=2), parseo de infoboxes genéricos.
    • data/import-tmdb.js — búsqueda de películas por personaje.
    • data/cache-images.js — pipeline de imágenes en 4 fuentes, validación por magic bytes (rechaza páginas de error guardadas como JPG), poda automática de cachés corruptos y de personajes reportados.
    • data/build-sqlite.js — reconstruye el .db desde los JSON semilla.
  • Sistema de batalla: tiers (Cosmic → Superhuman → Peak Human) detectados en secciones estructuradas + señales de identidad fuertes en la bio; poderes ponderados; suerte determinística por par de ids (strHash) para URLs compartibles; narrativa arcade generada por plantillas (sin IA).
  • Seguridad/prácticas: secrets solo en env (Fly secrets), CORS acotado, queries 100% parametrizadas, respeto de rate limits a terceros (SerpAPI, Wikimedia), fallback a placeholder local en imágenes rotas.
  • Tests: 49 verdes (27 API e2e + 11 unitarios de funciones puras + 11 e2e del sitio con procesos reales). npm test.

Deployment (Fly.io)

Demo pequeña → Fly.io free tier (3 VMs shared-cpu 256 MB; la API usa ~66 MB). Config: Dockerfile (node:24-slim), fly.toml (web :5050 público, API :3001 interna), entrypoint.sh (siembra el volumen con el .db + cache al primer arranque; cp -n sincroniza imágenes nuevas en cada deploy), .dockerignore (los JSON semilla NO entran a la imagen: solo el .db y el cache).

Bug real de producción resuelto: el volumen montado en /app/data ocultaba los scripts data/*.js de la imagen → require en top-level fallaba con MODULE_NOT_FOUND. Solución: require perezoso de build-sqlite.js (solo si el .db falta) + scripts de mantenimiento copiados a /app/data-scripts/ (usables vía fly ssh console con DB_PATH apuntando al volumen).

fly apps create marvelousdb
fly volumes create marvelousdb_data --size 3 --region ams --yes
fly secrets set SERPAPI_KEY=... TMDB_AUTH_HEADER=... CLOUDFLARE_API_TOKEN=...
fly deploy

4. La parte divertida 🎮

  • VS Battle (/vs/cthulhu-vs-u-s-agent): busca por trigram, tiers de poder, barras comparativas, perdedor debilitado (gris, inclinado, como el faint de Pokémon), etiquetas WINNER (dorado neón) / LOSER.
  • BATTLE REPORT arcade, determinista por pareja: “Cthulhu used CULT FOLLOWING! SLAM!”, la última frase del perdedor como “last words as the dust clears”, onomatopeyas animadas (POW!/WHAM!/BOOM!).
  • Lore generado: personajes sin bio reciben un párrafo pulp estable (“Some say Loki was never real…”).
  • Las decisiones se guardan como “notas de campo” en ~/nef/decisions/.

5. Números finales

MétricaValor
Personajes10,819 (9,417 dominio público)
Cómics30,179
Tamaño SQLite269 MB (1 archivo portable)
Imágenes cacheadas localmente183 (pipeline Fandom/Commons/BrowserRun/SerpAPI)
Tests51, 0 fallos
API~0.5 s de arranque, ~66 MB RSS
Fuentes de datosPDSH (MediaWiki), Marvel seed 2014, TMDB, Fandom, Commons, Browser Run, SerpAPI
Producciónhttps://marvelousdb.fly.dev/ (volumen persistente 3 GB)

6. Pendientes

  • Commitear los ~10,073 archivos nuevos (el repo sigue con el commit de 2014).
  • Terminar el cacheo de imágenes (faltan ~420; los reportados se re-cachean solos).
  • Subir el límite de imágenes de los personajes Marvel si se quiere 100%.
  • (Actualizado: el repo ya está committeado y pusheado en https://github.com/[usuario]/marvelous — los pendientes reales son el cacheo de imágenes y el FLY_API_TOKEN para el cron de sueño/despertar.)

7. Decisiones a fondo (para revisión cuasi-forense de pares)

Cada decisión sigue el formato: Contexto → Decisión → Alternativas descartadas → Trade-offs → Evidencia en el repo. La evidencia es verificable: los 51 tests, los endpoints y los scripts citados.

7.1 Dos procesos (API + web) en vez de una sola app Express

  • Contexto: el demo original era un solo proceso Express 3 que hablaba directo con Orchestrate. Al quitar Orchestrate había que decidir la forma de la capa de datos.
  • Decisión: api.js (puerto 3001, solo localhost) + web.js (puerto 5050, público) que consume la API por HTTP y la proxifica (/api/*).
  • Alternativas descartadas: (a) un solo proceso — se eligió dos porque la separación permite reutilizar la API fuera del sitio (curl, scripts, tests e2e contra procesos reales) y porque el usuario pidió explícitamente “API REST separada”; (b) Redis/DB en red — innecesario a esta escala (ver 7.3).
  • Trade-offs: dos procesos que arrancar (resuelto con concurrently en dev y un entrypoint.sh en prod); el proxy añade ~1ms de latencia local.
  • Evidencia: api.js, web.js, entrypoint.sh, tests web.test.js.

7.2 node:sqlite + FTS5 trigram como motor (sin ORM, sin librería de búsqueda)

  • Contexto: la primera versión cargaba 40,000 JSON en memoria e indexaba con FlexSearch (~7s de arranque, ~500 MB RSS). El usuario pidió “un solo archivo .sqlite portable”.
  • Decisión: SQLite embebido vía node:sqlite (módulo built-in de Node ≥ 23.4), FTS5 con tokenizer trigram para búsqueda substring case-insensitive, orden por bm25. Nada en memoria: 0.5s de arranque, ~66 MB RSS. Se eliminó FlexSearch (una dependencia menos).
  • Alternativas descartadas: (a) better-sqlite3 — excelente, pero es compilación nativa y node:sqlite ya viene en el runtime (local-first, cero deps); (b) D1 de Cloudflare — no corre fuera de Workers y habría atado el deploy a Cloudflare; (c) FlexSearch — la búsqueda en SQL elimina la carga en RAM y el índice vive en el archivo.
  • Trade-offs: trigram no matchea términos < 3 caracteres (fallback LIKE en nombre/título); el índice trigram ocupa más espacio (aceptable: 269 MB totales); node:sqlite era “experimental” → se silencia el warning con --disable-warning y se fija engines >= 23.4.
  • Evidencia: api.js (búsquedas), data/build-sqlite.js (esquema FTS5), test/api.test.js.

7.3 Por qué no Redis ni un caché externo

  • Decisión: los datos derivados viven en archivos JSON del lado del volumen (img-cache.json, tmdb-films.json, reported-images.json) + el filesystem (public/img/cache/), y la BD en SQLite.
  • Razón: un solo proceso local con ~66 MB de RAM no justifica un servidor de caché extra (operación, contenedor, secrets). Es la filosofía “baterías incluidas” que el propio usuario mencionó al preguntar por Redis. Si hubiera que servir a muchos usuarios, el paso natural sería el CDN de Cloudflare en el dominio de Fly, no Redis.
  • Trade-offs: sin TTL ni invalidez global; el caché de imágenes manda siempre (bueno para el hotlinking de wikia, ver 7.5).
  • Evidencia: api.js (loadImgCache/loadFilms/loadReported), README.md.

7.4 El dataset de dominio público: importar el wiki PDSH con el esquema heredado

  • Contexto: el seed de Marvel (2014) define el esquema (name, wiki, thumbnail, comics.items). Conservarlo permite que ambos datasets convivan en una sola BD.
  • Decisión: data/import-pdsh.js usa la MediaWiki Action API de Fandom (allpages → revisions → pageimages, formatversion=2) y traduce los infoboxes genéricos al esquema existente (real_name, debut, publisher, created_by, bio limpia de markup wiki).
  • Alternativas descartadas: mcuapi (películas del MCU, con copyright) y Download-ComicBooks-API (material protegido) — evaluadas y rechazadas por licencia; Gutendex/Internet Archive/LoC no mapean imágenes por nombre de personaje.
  • Trade-offs: ~10,000 requests al wiki (incremental, reanudable); la calidad del texto varía (bios con markup wiki, personajes duplicados sin desambiguar).
  • Evidencia: data/import-pdsh.js, data/pdsh/characters/*.json.

7.5 El pipeline de imágenes y el bug del hotlinking de wikia

  • Contexto: el 99% de las imágenes PDSH se veían “vacías” en el navegador aunque la API las sirviera bien.
  • Diagnóstico (lección metodológica): los curl de prueba funcionaban (sin Referer), pero el navegador manda Referer: localhost:5050 y static.wikia.nocookie.net devuelve 404 al hotlink. El bug solo aparecía reproduciendo el tráfico real del navegador.
  • Decisión: (a) <meta name="referrer" content="no-referrer"> (fix inmediato); (b) pasada --wikia que descarga las 9,282 imágenes a public/img/cache/ y hace que el caché local mande sobre la URL remota (local-first + inmune a bloqueos futuros).
  • Evidencia: data/cache-images.js (wikiaPass), api.js (charThumb), views/layouts/main.handlebars.

7.6 Fuentes de imágenes en cascada: Fandom → Commons → Browser Run → SerpAPI

  • Razón: respetar costo 0 y licencias antes de gastar cuota paga:
    1. Fandom PDSH (gratis, sin key): la imagen del propio wiki del personaje.
    2. Wikimedia Commons (gratis): se valida la licencia por extmetadata (publicdomain/cc0/pdm) — el checklist legal del usuario.
    3. Cloudflare Browser Run (gratis, 10 min de navegador/día): scrapea DuckDuckGo Images vía /scrape; se descubrió que Google identifica el tráfico de Browser Run como bot y lo bloquea.
    4. SerpAPI (key, ~250/mes): última opción, la única vía legítima a Google Images.
  • Validación transversal: magic bytes (nunca guardar una página de error como JPG) y poda automática del índice (los thumbnails expirados de Google se registraban como HTML de ~400 bytes → detectados y re-encolados).
  • Evidencia: data/cache-images.js (4 pasadas + prune), README.md.

7.7 El sistema de batalla: tiers con “secciones estructuradas” que mandan

  • Contexto: el usuario objetó “¿cómo U.S. Agent le gana a Cthulhu?“. El primer sistema contaba keywords de poder sobre el texto completo.
  • Bug que calibra el diseño: la bio narrativa de Marvel (24 KB de prosa con Loki, Scarlet Witch, “elder god”, “dimensional rift”) daba tier divino a mortales. Se invirtió la fuente de verdad: tiers y poderes se leen de powers/abilities/categories (estructurado); la bio solo aporta señales de identidad (“is an Asgardian”, “worship”, “imprisoned for millennia”) y posesión (“possesses superhuman strength”).
  • Trade-offs: un personaje oscuro con bio pobre queda “Normal Human” aunque tenga un poder menor — aceptado, la consistencia importa más que el overfitting.
  • Evidencia: api.js (TIERS, POWER_PATTERNS, LORE_POWER_PATTERNS), test/unit.test.js (detectTier), test “Cthulhu outranks U.S. Agent”.

7.8 Suerte determinística por par (URLs compartibles)

  • Decisión: con idA+idB, la suerte = strHash(idA|idB) % 3 - 1; las batallas aleatorias conservan aleatoriedad. El resultado y la narrativa son función pura del par → /vs/cthulhu-vs-u-s-agent muestra SIEMPRE el mismo desenlace.
  • Razón: una URL compartida que cambia de ganador al recargarla no es compartible; el determinismo convierte el endpoint en una función pura cacheable y testeable (deepStrictEqual entre llamadas).
  • Evidencia: api.js (strHash, luckOverride), test/unit.test.js, test/web.test.js.

7.9 Lore y narrativa “sin IA”

  • Decisión: nada de LLM (local-first, sin costos, sin latencia). El lore usa el primer párrafo real de la bio y, si no existe, plantillas pulp elegidas por hash del id (estable entre visitas). La narrativa arcade combina plantillas por fase + onomatopeyas + quotes extraídas de las bios (oraciones 25-220 chars, prefiriendo diálogo, limpias de markup wiki).
  • Trade-offs: las plantillas se repiten entre personajes (aceptable); las quotes pueden salir de contexto (se eligen determinísticamente, no semánticamente).
  • Evidencia: api.js (characterLore, extractQuotes, battleNarrative), test/unit.test.js.

7.10 Tres capas de tests con procesos reales

  • Decisión: (1) e2e de API contra procesos reales (child processes con puertos efímeros); (2) unitarios de funciones puras exportadas (slugify, strHash, characterLore, battleNarrative, fightStats…); (3) e2e del web contra API+web reales (rutas, proxy POST/DELETE, render arcade).
  • Razón: los bugs más caros de la sesión (hotlinking, .env trackeado, mount del volumen que oculta data/*.js) NO aparecían en tests unitarios — aparecían al reproducir el tráfico real. Los e2e con procesos reales son la red que los atrapa.
  • Evidencia: test/api.test.js (27), test/unit.test.js (13), test/web.test.js (11).

7.11 Deploy en Fly.io y el bug del volumen que oculta código

  • Decisión: node:24-slim + volumen persistente en /app/data (el .db y el cache viven fuera de la imagen; el entrypoint siembra el volumen en el primer arranque con cp -n).
  • Bug real: el mount en /app/data oculta los scripts data/*.js de la imagen → require('./data/build-sqlite.js') en top-level fallaba con MODULE_NOT_FOUND en producción (funcionaba en local, donde no hay mount). Se resolvió con require perezoso (solo si el .db falta) + scripts de mantenimiento en /app/data-scripts/ (fuera del mount).
  • Trade-offs: la imagen pesa 321 MB (el .db se sube como seed); los cambios al .db requieren re-deploy o rebuild en el servidor.
  • Evidencia: Dockerfile, fly.toml, entrypoint.sh, api.js (ensureDb).

7.12 Candados anti-abuso para billing 0

  • Decisión: rate limit por IP (120/min, ventana deslizante en memoria, configurable), Cache-Control: max-age=7d en production, API interna no publicada, secrets solo en Fly secrets, y cron de sueño/despertar (GitHub Actions gratuito, 23:00-08:00 CDMX) o stop manual.
  • Razón: en el plan free de Fly lo que cuesta es el egress; un script externo golpeando la API o re-descargando imágenes lo quema. El rate limit
    • cache headers reducen el egress a una fracción.
  • Trade-offs: un rate limit en memoria no persiste entre reinicios (la ventana se reinicia) — suficiente para disuadir, no para defenderse de un DDoS real (eso sería Cloudflare delante).
  • Evidencia: web.js (createRateLimiter), .github/workflows/*.yaml, README.md.

7.13 Cosas que haría distinto (honestidad técnica)

  • Los selects de 10,000 opciones del primer comparador (reemplazados por autocomplete trigram) — el rendimiento percibido importa.
  • Los JSON derivados (img-cache.json, tmdb-films.json) podrían vivir como tablas en SQLite; los dejé como archivos para sobrevivir rebuilds — hay un trade-off de consistencia que un par podría señalar con razón.
  • El índice FTS5 trigram no es un buscador semántico: “traje” no matchea “armadura”. Para una demo está bien; para un producto, habría que añadir sinónimos o embeddings.
  • El .env estaba trackeado desde el commit de 2014: se eliminó con git rm --cached, pero el histórico lo conserva (solo contenía un placeholder, no secretos reales).
  • Los tests cubren la funcionalidad, no el rendimiento: no hay benchmark de la búsqueda bajo carga concurrente.

8. Cómo auditar este repo (guía rápida para pares)

  1. npm test → 51 tests, 0 fallos (3 capas).
  2. node data/build-sqlite.js → reconstruye el .db desde los JSON (11s).
  3. node data/import-pdsh.js --dry-run --max 400 → valida el importador sin escribir.
  4. Endpoints: GET /api/health, /api/stats, /api/battle?idA=7005&idB=1009682 (determinista), /api/characters/by-slug?slug=cthulhu.
  5. fly deploy → despliega la demo; fly machine stop -a marvelousdb → la apaga (billing 0).
  6. El historial de decisiones de proceso está en ~/nef/decisions/ (14 notas fechadas).