> ## Documentation Index
> Fetch the complete documentation index at: https://vantsgg.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Documentación interna de VANTS, la plataforma de esports, escrita en español. Cubre siete repositorios de la organización de GitHub VANTSNET y dos de la cuenta feispla: el prototipo VANTSNET, que se llama igual que la organización, y vantsoport.
> Empieza por /mapa-del-codigo para saber qué repositorio y qué carpeta tocar, y consulta /glosario para los términos propios como pt_, mm_, RR, VANTS-BUS o VANTCALL.
> Cuando un componente existe en varios repositorios, estas páginas describen la versión de Vantsgames salvo que digan otra cosa. El esquema de la base de datos se versiona en VantsBasededatos.
> La web y la API pública están en https://vants.vants.workers.dev. Rutas y respuestas: /referencia/api-publica. Tablas y estados: /referencia/esquema-base-de-datos.
> No inventes rutas, tablas, comandos ni variables de entorno: si algo no aparece aquí, revisa el código del repositorio correspondiente.

# Esquema de la base de datos

> Tablas, columnas clave, relaciones y estados de la base de datos de VANTS en Neon por área: torneos, matchmaking, ranked, bus de eventos, agente y bot.

La base de datos de VANTS es un Postgres en Neon. Su esquema se versiona como migraciones SQL en [VantsBasededatos](https://vantsgg.mintlify.site/repos/vants-base-de-datos.md). Esta página resume qué guarda cada área y cómo se relacionan las tablas principales.

> [!NOTE]
>
> Datos al 23 de octubre de 2026: 69 tablas en producción y 34 migraciones. Las migraciones de este repositorio crean 39 de esas tablas. Las otras 30, como `players`, `seasons` y `season_player_stats`, no las crea ninguna migración: las migraciones solo las referencian o las modifican. Las migraciones son la fuente de verdad: si algo no coincide con esta página, manda el SQL.

## Mapa por áreas

| Área | Tablas | Qué guarda |
| - | - | - |
| [Plataforma de torneos](#plataforma-de-torneos) | `pt_games`, `pt_torneos`, `pt_teams`, `pt_matches`, `pt_players`, `pt_articulos` | Catálogo público de juegos, torneos, equipos, partidas, jugadores y artículos |
| [Torneos de VANTS y presencia](#torneos-de-vants-y-presencia) | `tournament_teams`, `tournament_matches`, `presence` | Inscripciones, cuadro de eliminatorias y quién está conectado |
| [Matchmaking](#matchmaking) | `mm_tickets`, `mm_matches`, `mm_match_players`, `mm_invites`, `mm_ready` | Cola, partidas, participantes, invitaciones y confirmaciones |
| [Ranked y FACEIT](#ranked-y-faceit) | `series_ranked`, `faceit_matches`, `faceit_webhook_events`, `rr_historial` | Series, partidas de FACEIT, XP e historial de RR |
| [Bus de eventos](#bus-de-eventos) | `vants_events`, `vants_consumers`, `vants_bus_dedup`, `vants_feed` | Eventos que se intercambian los proyectos |
| [Agente](#agente) | `agent_runs`, `agent_events`, `agent_comandos`, `agent_memoria`, `agent_archivos`, `agent_sandbox_execs` | Vigilancia automática de la plataforma |
| [Bot y eventos web](#bot-y-eventos-web) | `bot_heartbeat`, `bot_settings`, `discord_channels`, `bot_alerts`, `vant_sync_events`, `web_events` | Estado del bot, alertas y eventos de la web |
| Jugadores y temporadas | `players`, `seasons`, `season_player_stats`, `profiles` | Perfiles, temporadas y estadísticas por temporada (rating, MMR, rango, victorias y derrotas) |
| Cuentas y pagos | `vants_links`, `purchases`, `entitlements`, `audit_logs` | Cuentas externas vinculadas, compras, accesos y auditoría |

## Convenciones

* Las claves primarias son `uuid` generados con `gen_random_uuid()`, salvo claves naturales como `slug`, `ticket` o `event_id`.
* Las fechas son `timestamptz`.
* Los estados son `text` con una restricción `CHECK` que lista los valores permitidos.
* Los datos de demostración llevan `is_demo = true` y se pueden ocultar con ese filtro.
* Los nombres de columnas mezclan español e inglés, según la migración que los creó.

## Plataforma de torneos

Catálogo público con prefijo `pt_` (migraciones 28 y 29). Lo lee la [API pública](https://vantsgg.mintlify.site/referencia/api-publica.md) y es independiente de los torneos propios de VANTS. Cada torneo, partida y jugador indica su origen en `source`.

```mermaid theme={null}
erDiagram
  pt_games ||--o{ pt_torneos : "game_slug"
  pt_games ||--o{ pt_matches : "game_slug"
  pt_games |o--o{ pt_teams : "game_slug"
  pt_games |o--o{ pt_players : "game_slug"
  pt_games |o--o{ pt_articulos : "game_slug"
  pt_torneos |o--o{ pt_matches : "tournament_id"
  pt_teams |o..o{ pt_players : "team_slug"

  pt_games {
    text slug PK
    text nombre
    text corto
    boolean activo
    int orden
  }
  pt_torneos {
    uuid id PK
    text slug UK
    text game_slug FK
    text nombre
    text estado
    text region
    numeric prize_value
    timestamptz starts_at
    timestamptz ends_at
  }
  pt_teams {
    uuid id PK
    text slug UK
    text nombre
    text tag
    text game_slug FK
  }
  pt_matches {
    uuid id PK
    text game_slug FK
    uuid tournament_id FK
    text equipo_a
    text equipo_b
    int score_a
    int score_b
    text estado
    timestamptz empieza_at
  }
  pt_players {
    uuid id PK
    text slug UK
    text nombre
    text game_slug FK
    text team_slug
    text rol
  }
  pt_articulos {
    uuid id PK
    text slug UK
    text titulo
    text game_slug FK
    text estado
    timestamptz publicado_at
  }
```

El diagrama muestra solo las columnas principales. Tres detalles que conviene conocer:

* `pt_matches.equipo_a` y `equipo_b` guardan el nombre del equipo como texto, no una clave foránea.
* `pt_players.team_slug` apunta a `pt_teams.slug` sin clave foránea, así que borrar un equipo no toca a sus jugadores.
* Torneos, partidas y jugadores son únicos por (`source`, `external_id`), lo que permite importar datos externos sin duplicarlos.

Juegos registrados en `pt_games`: `valorant`, `cs2` y `dota2` están activos; `lol`, `r6s` y `cod` existen pero están inactivos. No se puede borrar un juego que tenga torneos o partidas.

| Campo | Valores permitidos |
| - | - |
| `source` (torneos, partidas y jugadores) | `vants`, `faceit`, `demo` |
| `pt_torneos.estado` | `upcoming`, `ongoing`, `finished`, `cancelled` |
| `pt_torneos.location_type` | `online`, `lan` |
| `pt_matches.estado` | `upcoming`, `live`, `finished`, `cancelled` |
| `pt_articulos.estado` | `draft`, `published` |

## Torneos de VANTS y presencia

Los torneos propios de VANTS usan sus propias tablas (migración 17). `tournament_id` es un identificador de texto, no una clave foránea a `pt_torneos`.

**`tournament_teams`** guarda las inscripciones.

| Columna | Detalle |
| - | - |
| `name` | De 2 a 32 caracteres. No se admiten dos equipos con el mismo nombre en un torneo, sin distinguir mayúsculas |
| `tag` | De 2 a 4 caracteres |
| `game` | Por defecto `valorant` |
| `captain`, `region` | Capitán y región del equipo |
| `status` | `pendiente`, `confirmado` o `rechazado` |

**`tournament_matches`** guarda el cuadro. Cada fila es única por (`tournament_id`, `ronda`, `slot`).

| Columna | Detalle |
| - | - |
| `ronda` | `q`, `s`, `f`, `l1`, `l2` o `lf` |
| `slot` | De 0 a 3 |
| `team_a`, `team_b` | Nombres de los equipos |
| `score_a`, `score_b` | Marcador |
| `estado` | `pendiente`, `en_curso` o `final` |
| `ganador` | Equipo ganador. El cuadro avanza cuando la partida está en `final` y tiene ganador |

### Cuadro de 8 equipos

El cuadro es de doble eliminación. Cuando un torneo llega a 8 equipos confirmados, un trigger lo siembra una sola vez: los 8 primeros por orden de inscripción forman los 4 cruces de cuartos y se crean vacías el resto de partidas. Con menos de 8 equipos confirmados no hay cuadro.

Al marcar una partida como `final` con su `ganador`, otro trigger mueve a los equipos:

| Ronda | Slots | El ganador pasa a | El perdedor pasa a |
| - | - | - | - |
| `q` (cuartos) | 0 a 3 | `s` | `l1` |
| `s` (semifinales) | 0 y 1 | `f` | `l2` |
| `l1` (inferiores, ronda 1) | 0 y 1 | `l2` | Queda eliminado |
| `l2` (inferiores, ronda 2) | 0 y 1 | `lf` | Queda eliminado |
| `lf` (final inferior) | 0 | `f`, si ese hueco sigue libre | Queda eliminado |

La gran final (`f`, slot 0) cierra el cuadro. Si un equipo no avanza solo, el propietario puede colocarlo a mano.

### Presencia

`presence` tiene una fila por jugador conectado.

| Columna | Detalle |
| - | - |
| `player_id` | Clave primaria, referencia a `players` |
| `estado` | `online`, `jugando` o `ausente` |
| `juego` | Juego actual |
| `actualizado` | Se renueva en cada actualización |

La web y la app de escritorio envían un latido cada 60 segundos.

### Vistas

Para mostrar datos públicos usa estas vistas en lugar de las tablas.

| Vista | Qué devuelve |
| - | - |
| `v_en_vivo` | Jugadores con presencia en los últimos 120 segundos y estado distinto de `ausente`, con sus datos básicos y su MMR más reciente |
| `v_torneo_equipos` | Equipos inscritos en un torneo, sin los rechazados y sin datos de quien inscribe |
| `v_torneo_bracket` | Todas las partidas del cuadro |

## Matchmaking

El matchmaking persiste la cola en Postgres (migraciones 21 a 24, 30 a 32) para que sobreviva a los reinicios y el emparejamiento sea transaccional. Cada fila guarda además la identidad del jugador; esas columnas no se detallan aquí.

```mermaid theme={null}
erDiagram
  mm_matches ||--o{ mm_match_players : "match_id"
  mm_matches ||--o{ mm_ready : "match_id"
  mm_matches |o..o{ mm_tickets : "match_id"

  mm_matches {
    uuid id PK
    text game
    text mode
    text region
    text status
    int score_a
    int score_b
    timestamptz ready_deadline
  }
  mm_match_players {
    uuid match_id PK, FK
    smallint team
    int rr_before
    int rr_after
  }
  mm_ready {
    uuid match_id PK, FK
    timestamptz listo_at
  }
  mm_tickets {
    text ticket PK
    text game
    text mode
    text region
    text status
    uuid match_id
    timestamptz expires_at
  }
  mm_invites {
    uuid id PK
    text game
    text mode
    text region
    text status
    timestamptz expires_at
  }
```

`mm_tickets.match_id` no es una clave foránea: es la referencia a la partida a la que se asignó el ticket.

### Modos

| Modo | Jugadores por partida |
| - | - |
| `duo` | 2 (1v1) |
| `team` | 10 (5v5) |

Cada ticket indica `game`, `mode` y `region` (por defecto `EU`). Un jugador solo puede tener un ticket activo por juego. El emparejador toma los primeros jugadores de la cola por orden de llegada (FIFO) y crea la partida solo si hay exactamente los necesarios, sin bloquear a otros emparejadores en paralelo.

### Estados

Ticket:

```mermaid theme={null}
flowchart LR
  queued --> matched
  queued --> cancelled
  queued --> expired
```

Partida:

```mermaid theme={null}
flowchart LR
  propuesto --> reportado --> verificado
  propuesto --> anulado
```

* `propuesto`: el emparejador creó la partida y fijó `ready_deadline` a 120 segundos.
* `reportado` y `verificado`: un participante reporta el marcador, se aplica el RR a cada jugador y se guardan `rr_before` y `rr_after`. Aplicar el resultado es idempotente.
* `anulado`: venció el plazo sin que todos confirmaran. La anulación es perezosa: la dispara el primer participante que consulta la partida después del plazo. Los tickets quedan como historial y los jugadores pueden volver a ponerse en cola.

### Invitaciones

`mm_invites` guarda invitaciones entre jugadores para jugar `duo` o `team`. Caducan a los 10 minutos y pasan de `pendiente` a `aceptada`, `rechazada`, `cancelada` o `expirada`. Solo puede haber una pendiente por pareja de jugadores y juego.

### Funciones

El cliente web y la app de escritorio usan las mismas funciones SQL, de modo que hay una sola fuente de verdad.

| Función | Qué hace |
| - | - |
| `mm_encolar` | Pone al jugador en cola y devuelve su ticket, reutilizando uno vivo si existe |
| `mm_ticket_estado` | Devuelve la posición, el estado y la partida del ticket. En cola añade el total de tickets vivos y cuántos jugadores hacen falta |
| `mm_salir` | Saca al jugador de la cola |
| `mm_listo` | Confirma la participación en una partida propuesta |
| `mm_estado_ready` | Devuelve cuántos participantes están listos, el plazo y si ya venció |
| `mm_anular_si_timeout` | Anula la partida si venció el plazo sin todas las confirmaciones |
| `mm_reportar` | Reporta el marcador y aplica el RR |
| `mm_historial_de` | Devuelve el historial de RR de un jugador |
| `mm_invitar` | Envía una invitación a otro jugador, al que se localiza por su nick |
| `mm_invites_pendientes` | Lista las invitaciones pendientes |
| `mm_responder_invite` | Acepta o rechaza una invitación |

La vista `mm_historial` reúne, por jugador, las partidas en las que participó, con su equipo, el marcador, el RR antes y después y el resultado.

## Ranked y FACEIT

### Escala de rango

El rango se calcula a partir del rating del jugador. El RR que se muestra es el rating multiplicado por 100.

| Rating mínimo | Rango |
| - | - |
| Menos de 0.95 | FUNDICIÓN (solo en la web de vants-dev-launcher) |
| 0.95 | ACERO |
| 1.00 | TITANIO |
| 1.05 | OBSIDIANA |
| 1.11 | CROMO |
| 1.17 | PLASMA |
| 1.24 | ÉTER |
| 1.30 | VANTS |

La web de vants-dev-launcher muestra el rango VANTS con el nombre APEX. La web de Vantsgames no muestra estos nombres: en `/ranking#rangos` presenta VANTS RANKED, una escalera de 9 rangos por XP que va de CENIZA (0 XP) a SUPREMO (8000 XP) en pasos de 1000 XP. Está definida en `vants-server/public/ranking.html` y `vants-server/public/static/js/perfil_pagina.js`.

Sin rating no hay rango y el jugador está en partidas de colocación, que parten de 0.95. Las partidas de FACEIT y las internas aplican la misma regla (migraciones 10 y 22):

| Resultado | Cambio de rating |
| - | - |
| Victoria | +0.015 |
| Empate | +0.002 |
| Derrota | -0.012 |

El rating siempre queda entre 0.50 y 1.60.

### Series

`series_ranked` guarda series entre un retador y un rival. Las reporta un jugador y las confirma el rival o el propietario (`pendiente`, `confirmada` o `rechazada`). Al confirmarse, se aplica el Elo a `players.rating` y se actualizan victorias, derrotas, MMR y rango en `season_player_stats`. La tabla guarda el rating de ambos antes y después. Las funciones son `reportar_serie`, `confirmar_serie`, `rechazar_serie` y `mis_series`.

### FACEIT

* `faceit_matches`: una fila por partida de FACEIT ya procesada. `faceit_match_id` es único, así que una partida nunca da XP dos veces. Guarda `result` (`win`, `loss` o `draw`), `xp_awarded` y `elo_delta`.
* `faceit_webhook_events`: eventos crudos que llegan por webhook, con su `payload` y la marca `processed`. Las funciones `faceit_procesar_eventos` y `faceit_aplicar_resultado` los convierten en partidas.

La experiencia sube +100 XP por victoria y +25 por empate; la derrota no suma. El nivel es `1 + floor(xp / 500)`. El procesador soporta `cs2` (`csgo` se normaliza a `cs2`) y `valorant`.

### Historial de RR

`rr_historial` registra cada cambio de rating, venga de donde venga.

| Columna | Detalle |
| - | - |
| `motivo` | `faceit_partida`, `match_interno` o `ajuste_manual` |
| `fuente` | `faceit`, `interno` o `manual` |
| `partida_id` | Identificador de la partida de origen |
| `temporada` | Temporada activa en ese momento |
| `game`, `mode`, `region` | Contexto de la partida |
| `rr_before`, `rr_after` | RR antes y después |
| `rr_delta` | Columna generada: `rr_after - rr_before` |

Es única por (`player_id`, `fuente`, `partida_id`), así que repetir una llamada no duplica líneas. Las vistas `rr_historial_jugador` y `rr_resumen` leen de ella, y `rr_temporada_actual` devuelve la temporada vigente.

## Bus de eventos

Los proyectos de VANTS se comunican con eventos inmutables (migraciones 33 y 34). El diagrama sigue un resultado de FACEIT desde `faceit_webhook_events` (migración 09) hasta quienes leen el bus. En mayúsculas aparece lo que no es una tabla: FACEIT y los cursores de flask-agents y de VANTS HUB, que cada uno guarda en un archivo local.

```mermaid theme={null}
erDiagram
  FACEIT ||--o{ faceit_webhook_events : "envía webhooks"
  faceit_webhook_events ||--o{ vants_bus_dedup : "evita duplicados por event_id"
  faceit_webhook_events ||--o{ vants_events : "el puente publica match.*"
  vants_events ||--o{ vants_feed : "vants-consumer-playvants copia"
  vants_events ||--o{ CURSOR_FLASK_AGENTS : "flask-agents lee con su cursor"
  vants_events ||--o{ CURSOR_VANTS_HUB : "VANTS HUB lee con su cursor"

  faceit_webhook_events {
    bigserial id PK
    text event_type
    jsonb payload
    timestamptz created_at
    boolean processed
  }
  vants_bus_dedup {
    text event_id PK
    timestamptz seen_at
  }
  vants_events {
    text id PK "evt_ + ULID"
    text type "match.finished, post.generated..."
    text source "faceit-webhook, flask-agents..."
    timestamptz ts
    jsonb data
    timestamptz created_at
  }
  vants_feed {
    text id PK "mismo id del evento del bus"
    text type
    text source
    timestamptz ts
    jsonb data
    timestamptz consumed_at "se conserva 7 días"
  }
  CURSOR_FLASK_AGENTS {
    json cursor "archivo local, ruta en VANTS_BUS_CURSOR"
  }
  CURSOR_VANTS_HUB {
    json cursor "archivo local en userData de Electron"
  }
  vants_consumers {
    text project_id PK
    text cursor
    timestamptz updated_at
  }
```

| Tabla | Para qué sirve |
| - | - |
| `vants_events` | Registro de eventos: `id` (`evt_` más un ULID), `type`, `source`, `ts` y `data` en `jsonb`. Se conservan 30 días |
| `vants_consumers` | Posición de lectura (`cursor`) de cada proyecto consumidor. Ningún cliente la usa todavía: flask-agents y VANTS HUB guardan su cursor en un archivo local, y `vants-consumer-playvants` lo calcula a partir de `vants_feed` |
| `vants_bus_dedup` | Eventos ya vistos, para no procesar dos veces el mismo (por ejemplo, el webhook de FACEIT) |
| `vants_feed` | Eventos ya ordenados y listos para mostrar en la web (resultados, torneos, clips, alertas), con la marca `consumed_at`. Se conservan 7 días. La llena `vants-consumer-playvants`; la web todavía no la lee |

Tipos de evento de ejemplo: `match.finished`, `clip.published` y `alert.detected`.

## Agente

El agente vigila la plataforma y deja su trabajo en estas tablas (migraciones 25 y 26).

| Tabla | Qué guarda |
| - | - |
| `agent_runs` | Cada ejecución: fuente (`ci`, `api`, `cli` o `discord`), modo (`ciclo`, `informe` o `comando`), estado global (`UP`, `DEGRADED`, `DOWN` o `DESCONOCIDO`), hallazgos, acciones y resumen |
| `agent_events` | Incidencias detectadas, con `dedupe_key` única, severidad (`info`, `warn`, `error` o `critical`) y estado (`abierto`, `gestionando`, `resuelto` o `archivado`) |
| `agent_comandos` | Órdenes enviadas con `/agente comando`, su estado (`pendiente`, `hecho` o `error`) y la respuesta que entrega el bot por mensaje directo |
| `agent_memoria` | Memoria del agente por clave: tipo (`hecho`, `aprendizaje`, `preferencia` o `nota`), importancia de 1 a 5 y caducidad opcional |
| `agent_archivos` | Archivos que genera el agente, únicos por ruta, con su tamaño y huella |
| `agent_sandbox_execs` | Comandos ejecutados en un entorno aislado, con su código de salida y duración |

La vista `agent_resumen` cuenta las incidencias abiertas, los comandos pendientes y el estado de la última ejecución. La vista `agent_memoria_resumen` cuenta los recuerdos, los archivos y las ejecuciones.

## Bot y eventos web

Tablas de la migración 02 y de la migración 01.

| Tabla | Qué guarda |
| - | - |
| `bot_heartbeat` | Último latido del bot: `last_seen`, `latency_ms`, número de servidores y versión |
| `bot_settings` | Interruptores del bot por clave (`key`, `enabled`) |
| `discord_channels` | Canal de Discord que corresponde a cada categoría de aviso |
| `bot_alerts` | Alertas pendientes de enviar, únicas por (`kind`, `ref_id`), con su estado, intentos y error |
| `vant_sync_events` | Eventos de la web que se sincronizan con Discord, con el estado de cada intento de entrega |
| `web_events` | Eventos que la web envía al Supervisor Bot: tipo, severidad, página y estado de entrega a Discord |

El detalle de los comandos del bot está en [Bot de Discord](https://vantsgg.mintlify.site/referencia/bot-de-discord.md).

## Otras tablas

Hay además tablas en producción que ninguna migración crea. Por su nombre, parecen pertenecer a estas áreas. Revisa la base de datos antes de depender de ellas:

* Soporte: `support_tickets`, `support_ticket_messages`, `tickets`.
* Eventos y comunidad: `events`, `event_rsvps`, `postulaciones`.
* Pase y planes: `pass_progreso`, `pass_recompensas`, `plan_content`.
* Ranked: `ranked_rules`, `ranked_matches`, `ranked_history`.
* Torneos e inscripciones: `tournaments`, `tournament_entries`.

Las tablas de administración y de cuentas vinculadas son internas y no se documentan aquí.

## Cómo añadir una migración

Crea un archivo nuevo en `db/migrations/` con el siguiente número y escríbelo de forma idempotente. El flujo completo, con pruebas en una rama de Neon, está en [VantsBasededatos](https://vantsgg.mintlify.site/repos/vants-base-de-datos.md).

## Más información

* [VantsBasededatos](https://vantsgg.mintlify.site/repos/vants-base-de-datos.md): estructura del repositorio y flujo de trabajo
* [API pública](https://vantsgg.mintlify.site/referencia/api-publica.md): rutas que leen estas tablas
* [Arquitectura](https://vantsgg.mintlify.site/arquitectura.md): cómo encaja la base de datos con el resto de VANTS
* [vants-faceit-webhook](https://vantsgg.mintlify.site/repos/vants-faceit-webhook.md): origen de los eventos de FACEIT


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.