Plataforma de streaming de música libre (Creative Commons, artistas independientes y dominio público) con perfil de usuario, historial reproducible y un motor de recomendación híbrido propio basado en IA — contenido + colaborativo + contexto, con recomendaciones explicables.
https://soundmind-gateway.onrender.com
💤 Los servicios corren en free tier y se duermen; la primera carga puede tardar ~30 s en despertar.
Tras el cierre de la API de recomendaciones de Spotify (2024–2026), construir un recomendador musical con datos y APIs abiertas es un reto técnico real. SoundMind lo resuelve con:
- 🎧 Streaming 100% legal: catálogo Creative Commons de Jamendo, artistas independientes de Audius y dominio público / netlabels del Internet Archive — streaming completo. Nunca se almacena ni se hace proxy de audio (solo URLs oficiales de cada CDN). La legalidad se impone en toda la cadena: búsqueda, eventos y recomendaciones (con
CHECKen la base). - 👤 Perfil y eventos (F2): cuentas con JWT, likes, historial reproducible y eventos a Redis Streams — con consentimiento explícito (GDPR) y borrado total de cuenta.
- 🧠 IA híbrida propia (F3 + F4): embeddings de contenido + filtrado colaborativo (ALS) + re-ranking contextual, con recomendaciones explicables ("Porque escuchaste X", "Oyentes como tú…", "A esta hora sueles escuchar esto").
- 💸 Costo $0: todo corre en free tiers y open source.
ℹ️ Sobre el catálogo comercial: la música de major label no está disponible gratis y legalmente en ninguna API. SoundMind prioriza la legalidad por diseño; el catálogo comercial completo queda como evolución futura vía el SDK oficial de Spotify (cuenta Premium del usuario). Decisiones en
docs/adr(ADR-006 a ADR-013).
Microservicios ligeros con Clean / Hexagonal Architecture. Diagramas C4 en
docs/C4-DIAGRAMAS.md; narrativa completa en
docs/ARQUITECTURA.md y los diseños por fase
(F2 · F3 · F4).
flowchart LR
web["React SPA<br/>(Netlify)"] -->|REST /api/v1| gw["Gateway BFF<br/>auth · rate limit · proxy"]
gw --> music["Music<br/>Jamendo/Audius/Archive"]
gw --> users["Users<br/>perfil · likes · eventos"]
gw --> rec["Recommender<br/>Python/FastAPI"]
users --> pg[("PostgreSQL<br/>+ pgvector (Neon)")]
users --> redis[("Redis Streams<br/>(Upstash)")]
rec --> pg
cron["GitHub Actions<br/>embed + ALS (nocturno)"] --> pg
| Workspace | Rol | Stack |
|---|---|---|
apps/web |
SPA (auth, búsqueda, player, likes, historial, "Para ti") | React 19 + TypeScript + Vite + Tailwind 4 + Zustand + TanStack Query |
services/gateway |
BFF: auth, rate limit, proxy, cuenta | Node 22 + Express 5 + JWT RS256 + Argon2id + PostgreSQL + Redis |
services/music |
Catálogo y búsqueda multi-fuente | Node 22 + Express 5 + Zod |
services/users |
Perfil, likes, historial, eventos de escucha | Node 22 + Express 5 + PostgreSQL (Neon) + Redis Streams (Upstash) |
services/recommender |
IA híbrida (serving): coseno + producto interno en pgvector | Python 3.11 + FastAPI + pgvector |
jobs/embed, jobs/collab |
Batch nocturno (GitHub Actions): embeddings + ALS | Python (sentence-transformers, implicit) |
packages/shared |
Tipos de dominio compartidos | TypeScript |
Patrones aplicados (buscar PATTERN: / # PATTERN: en el código): Hexagonal Ports & Adapters, BFF, Adapter, Facade, Factory Method, Repository, Observer, Event-Driven (Redis Streams) y event sourcing ligero (snapshot inmutable de la pista en cada evento → historial reproducible).
Modelo híbrido en tres señales, con el patrón entrenar pesado / servir ligero: el entrenamiento (torch, ALS) corre en GitHub Actions y escribe vectores en pgvector; el servicio FastAPI en Render solo ejecuta álgebra de vectores (no lleva torch — no cabría en 512 MB). Ver ADR-012 y ADR-013.
| Señal | Cómo | Explicación al usuario |
|---|---|---|
| Contenido (F3) | embeddings de texto (all-MiniLM-L6-v2, 384-d) + coseno <=> |
"Porque escuchaste X" |
| Colaborativo (F4) | ALS (implicit) → factores latentes + producto interno <#> |
"Oyentes como tú lo escuchan" |
| Contextual (F4) | re-ranking por hora típica y racha de skips | "A esta hora sueles escuchar esto" |
El score final mezcla las señales con fallback a contenido cuando el
colaborativo no tiene datos, y resuelve el cold start con un onboarding de
géneros. Cada recomendación incluye un reason que la UI traduce a lenguaje
humano — diferenciador frente a las cajas negras comerciales.
- 🔎 Búsqueda multi-fuente con intercalado round-robin que preserva la relevancia de cada API.
▶️ Reproductor con cola, anterior/siguiente, volumen y registro automático de escuchas.- 👤 Registro / login (JWT RS256, refresh token rotativo en cookie
HttpOnly, store en Redis). - ❤️ Likes y "Mi historial" reproducible (cada evento guarda un snapshot de la pista).
- ✨ "Para ti" (recomendaciones híbridas), "Más como esta" y onboarding de géneros, con el porqué por pista.
- 🔒 Toggle de consentimiento de tracking y borrado de cuenta (
DELETE /me,ON DELETE CASCADE).
# 1. Instalar dependencias
npm install
# 2. Configurar entorno
cp .env.example .env
# - JAMENDO_CLIENT_ID: API key gratis en https://devportal.jamendo.com/
# - DATABASE_URL: PostgreSQL con pgvector (Neon free tier)
# - REDIS_URL: Redis (Upstash free tier)
# 3. Aplicar migraciones (crea tablas + extensión pgvector)
node --env-file=.env scripts/_apply-migrations.mjs
# 4. Levantar todo (gateway :4000, music :4002, users :4003, web :5173)
npm run devAbre http://localhost:5173, crea una cuenta, busca algo (ej. "lofi") y reproduce. 🎶
El Recommender (Python) y los batch son opcionales en local — ver
services/recommender/README.md.
| Pieza | Plataforma | Cómo |
|---|---|---|
apps/web |
Netlify | netlify.toml define build y redirects SPA. Configurar VITE_API_URL. |
gateway · music · users |
Render | render.yaml: 3 servicios Docker con healthchecks y autodeploy. |
recommender |
Render (Python) | Root services/recommender; uvicorn app.main:app. Ver su README. |
embed · collab |
GitHub Actions | Workflow train-recommender (cron nocturno + manual). Secret DATABASE_URL. |
| Base de datos / cola | Neon (PostgreSQL + pgvector) + Upstash (Redis) | Free tier. |
- 🔑 Claves JWT en Render: el panel rompe los PEM multilínea. El código acepta el PEM en base64 (recomendado) o en una línea con
\n:openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out jwt-private.pem openssl pkey -in jwt-private.pem -pubout -out jwt-public.pem base64 -w0 jwt-private.pem # → JWT_PRIVATE_KEY (gateway) base64 -w0 jwt-public.pem # → JWT_PUBLIC_KEY (gateway, users y recommender, el mismo)
- 🐘
DATABASE_URL: usar solo?sslmode=require. Quitar&channel_binding=require— rompe el driverpgen el Docker de Render. - 🐍 Python en Render: fijar la versión con
.python-version(3.12 da error con numpy/pgvector; el job ALS usa 3.11 porqueimplicitno tiene wheel para 3.12). - 🌐 CORS / cookies:
CORS_ORIGINS(gateway) → URL de Netlify; cookie de refreshSameSite=None; Secureen prod. - 💤 Cold start: el free tier de Render duerme tras 15 min — UptimeRobot hace ping a
/healthzcada 5 min. Los monitores mandanHEAD, así que/healthzaceptaGETyHEAD.
| Comando | Qué hace |
|---|---|
npm run dev |
Levanta los 4 servicios Node/web en paralelo |
npm run typecheck |
TypeScript estricto en todos los workspaces |
npm test |
Tests unitarios (Vitest); el recommender usa pytest |
npm run test:coverage |
Cobertura de dominio + aplicación con umbral 80% (CI la exige) |
node --env-file=.env scripts/_apply-migrations.mjs |
Aplica las migraciones SQL |
- El free tier es un requisito de diseño, no un límite molesto. No poder meter torch en 512 MB forzó la mejor decisión de la arquitectura: entrenar pesado en GitHub Actions, servir ligero en pgvector. El serving quedó minúsculo y rápido.
- La Arquitectura Hexagonal se pagó sola. Cambiar el repositorio de usuarios de in-memory → Postgres, el refresh-store → Redis, y la recomendación de contenido → híbrida se hizo tocando un adaptador y un caso de uso, con los tests intactos.
- Ser honesto con datos escasos. Con pocos usuarios el colaborativo es casi ruido; en vez de fingir señal, el híbrido cae a contenido y el sistema sigue siendo útil. El entregable es la arquitectura lista que mejora sola con volumen.
- Compilar no es verificar. Los bugs que importaron solo aparecieron contra la
realidad: un
ORDER BYcon alias que daba 500 (visto al correr la query en la DB real), un/healthzque rechazabaHEAD(visto al configurar el monitor), claves JWT que el panel rompía. Verificar en producción, no solotypecheck. - Coherencia de una decisión en toda la cadena. "Solo fuentes legales" no es un
comentario: se impone en la búsqueda, en la ingesta de eventos y en el corpus de
IA, con un
CHECKque lo blinda en la base.
- Iconos UI: Lucide (ISC). Música: Jamendo (CC), Audius (indie), Internet Archive (dominio público). Embeddings:
sentence-transformers. Sin assets generados por IA.
- F1 — MVP catálogo: búsqueda multi-fuente + reproducción + auth JWT + deploy
- F1.5 / F1.6 — Catálogo solo-legal: + Internet Archive; retirados Deezer y JioSaavn (ADR-009/010/011)
- F2 — Perfil y eventos: registro/login, likes, historial reproducible, Redis Streams, PostgreSQL, consentimiento (GDPR) — desplegado
- F3 — IA de contenido: embeddings + "más como esta" + onboarding — desplegado
- F4 — IA híbrida: colaborativo (ALS) + re-ranking contextual + explicabilidad — desplegado
- F5 — Pulido: diagramas C4, Dependabot + CodeQL, README + demo en vivo, cobertura de dominio >80% (CI la exige) y
npm auditlimpio
MIT © 2026 Maria del Carmen Salazar Torres