# 2. Arquitectura de software del backend

Oasis usa un patrón **MVC casero** (sin framework MVC):

```
  src/backend/backend.js   →  el ROUTER monolítico (Koa, ~7250 líneas, ~472 rutas)
  src/models/*_model.js    →  la lógica de DATOS (lee/escribe SSB)
  src/views/*_view.js      →  el RENDER de HTML (con hyperaxe)
```

Flujo de una funcionalidad:

```
  navegador → backend.js (ruta) → xModel.metodo() (SSB) → xView(data) (HTML) → ctx.body
```

---

## 2.1 Stack de software (dependencias clave)

Todas las dependencias están en `src/server/package.json`. Importante: casi
todos los `require` usan **ruta relativa explícita** al node_modules del
servidor, p.ej. `require("../server/node_modules/@koa/router")`.

**Capa web / HTTP**
- **`koa`** — servidor HTTP base. Se instancian varias apps Koa en
  `src/client/middleware.js` (principal + sub-apps para estáticos).
- **`@koa/router`** — enrutado. `backend.js:442` instancia
  `const router = new koaRouter()`. Toda la API es **una única cadena fluida**
  `router.param(...).get(...).post(...)` (líneas ~1270-7124).
- **`koa-static` / `koa-mount`** — sirven assets estáticos (`/assets`,
  `/maptiles`, `/game-assets`, `/js`...) montados en `middleware.js`.
- **`koa-body`** — parseo de `multipart/form-data` y POST. Se aplica por ruta:
  `.post('/votes/create', koaBody(), ...)`.
- `express` y `cors` figuran como dependencias pero **el flujo principal es
  Koa**; Express no se usa en el router.

**Motor de plantillas (vistas)**
- **`hyperaxe`** — hyperscript → HTML. **No hay archivos `.html`**; el HTML se
  construye programáticamente desde JS: `div(...)`, `nav(...)`, `form(...)`...
  Cada elemento expone `.outerHTML`.
- **`ssb-markdown` + `ssb-msgs` + `ssb-ref`** — render de Markdown SSB con
  resolución de menciones/enlaces internos (`src/views/markdown.js`).
- **`highlight.js`, `qrcode`, `moment`, `pretty-ms`** — sintaxis, QR, fechas.
- **`dompurify` + `jsdom` + `is-svg`** — saneado anti-XSS
  (`src/backend/sanitizeHtml.js`).

**Streams SSB**
- **`pull-stream`** — el paradigma de streams de SSB. Es el mecanismo central de
  lectura: `pull(source, pull.filter(...), pull.collect(cb))`.
- Auxiliares: `pull-paramap` (map asíncrono), `pull-sort`, `pull-abortable`
  (cancelar streams `live`), `pull-cat`, `pull-many`, `pull-pushable`.

**Cliente / servidor SSB**
- **`ssb-client`** — cliente RPC muxrpc para conectar al sbot (en `gui.js`).
- **`ssb-config`, `ssb-keys`, `ssb-ref`** — config, claves ed25519, validación
  de identificadores (`@feed`, `%msg`, `&blob`).
- **`secret-stack` + `ssb-caps` + `ssb-db`** y los plugins SSB → ver
  [01-red-ssb.md](01-red-ssb.md).

**Otros**: `module-alias`, `lodash`, `minimist`, `open`, `archiver`,
`file-type`, `pdfjs-dist`, `openpgp`, `axios`, `node-llama-cpp` +
`@xenova/transformers` (módulo IA local).

---

## 2.2 Flujo de una petición

**a) Construcción del servidor** — `src/client/middleware.js`: la función
exportada `({host, port, middleware, allowHost})` crea la app Koa, monta
estáticos, fija cabeceras de seguridad (CSP, `X-Frame-Options`...), valida el
host, aplica los middlewares recibidos y finalmente `app.listen({host, port})`
(línea 180). En `backend.js:7239`:
`const app = http({ host, port, middleware, allowHost: config.allowHost })`.

**b) Cadena de middleware** — `backend.js:7126-7238`: incluye (1) bloqueo de
no-GET en modo público, (2) `setLanguage(...)` por cookie, (3) gate de
indexación (muestra `indexingView` si la base no está sincronizada), (4) refresco
de estado compartido cada 60 s, y por último `routes` (las rutas del router).

**c) Ruta GET — ejemplo agenda** (`backend.js:1977-1982`):

```js
.get('/agenda', async (ctx) => {
  const filter = qf(ctx);
  let data = await agendaModel.listAgenda(filter);   // MODELO: lee SSB
  if (Array.isArray(data)) data = await applyListFilters(data, ctx);
  ctx.body = await agendaView(data, filter);          // VISTA: genera HTML
})
```

`agendaModel.listAgenda` abre el cliente (`cooler.open()`), lee el log
(`createLogStream`), agrega ítems y devuelve datos. `agendaView` los recibe y
construye HTML con hyperaxe, envuelto en `template(...)`. El HTML se asigna a
`ctx.body` y Koa lo sirve.

**d) Ruta POST — ejemplo votos** (`backend.js:5572`):

```js
.post('/votes/create', koaBody(), async ctx => {
  const b = ctx.request.body;
  const parsedOptions = b.options ? b.options.split(',')... : defaultOptions;
  await votesModel.createVote(stripDangerousTags(b.question), b.deadline, parsedOptions, ...);
  ctx.redirect(safeReturnTo(ctx, '/votes?filter=mine', ['/votes']));
})
```

El POST **sanea** la entrada (`stripDangerousTags`), llama al modelo que
**publica un mensaje SSB**, y **redirige** (patrón Post/Redirect/Get). El GET
posterior vuelve a leer del modelo y renderiza.

---

## 2.3 Conexión al servidor SSB: el "cooler"

El **cooler** es la abstracción que gestiona la conexión al sbot. Se define en
`src/client/gui.js` (importado en `backend.js:441` como
`const ssb = require("../client/gui")`) y se instancia en `backend.js:540`:

```js
const cooler = ssb({ offline: config.offline, port: config.port,
                     host: config.host, isPublic: config.public });
```

- Si existe un sbot **en el mismo proceso** (`internalSSB`), `cooler.open()` lo
  devuelve directamente sin RPC.
- Si no, conecta por muxrpc vía socket Unix `noauth` con backoff exponencial.

**Patrón de uso en todo el código**:
`const ssb = await cooler.open();` y luego se usan los métodos muxrpc:

| Método | Para qué |
|---|---|
| `ssb.createUserStream({id, reverse, limit})` | feed de un usuario |
| `ssb.createLogStream({reverse, limit})` | log global (lo más usado) |
| `ssb.query.read({query:[{$filter:{...}}]})` | consultas ssb-query |
| `ssb.messagesByType({type, private})` | mensajes por tipo |
| `ssb.backlinks.read({query})` | referencias/backlinks |
| `ssb.get(id, cb)` | un mensaje por id |
| `ssb.publish(content, cb)` | **publicar un mensaje** |
| `ssb.blobs.want/get/add` | blobs |
| `ssb.friends.isFollowing/isBlocking/graph` | grafo social |
| `ssb.conn.peers / ssb.status() / ssb.progress()` | estado de red |

---

## 2.4 Patrón de un MODELO

Todos los modelos siguen la **factory pattern**:
`module.exports = ({ cooler, isPublic, ...deps }) => ({ ...métodos })`. Reciben
el `cooler` por inyección desde `backend.js` (líneas 553-602). Muchos cachean el
cliente:

```js
let ssb;
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
```

**Publicar (escribir)** — `ssb.publish(content, cb)` con un campo `type`.
Ejemplo `votes_model.js` (`createVote`):

```js
const content = {
  type: 'votes', question, options, deadline, createdBy: userId,
  status: 'OPEN', votes: {...}, totalVotes: 0, createdAt: new Date().toISOString()
};
return new Promise((res, rej) =>
  ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg)));
```

**Leer** — `pull-stream` + `collect`:

```js
pull(
  ssbClient.createLogStream({ limit: logLimit }),
  pull.collect((err, results) => err ? reject(err) : resolve(results))
);
```

Luego se procesa en memoria: se construye un índice, se filtra por `type`, se
ordena.

**Borrar / editar — modelo de tombstone** (el log es *append-only*: nada se
borra físicamente):

- **Borrado lógico**: se publica `{ type:'tombstone', target:<msgId>, deletedAt,
  author }`.
- **Validación** (`src/models/tombstone_validator.js`,
  `buildValidatedTombstoneSet`): solo acepta el tombstone si **el autor del
  tombstone coincide con el autor del mensaje objetivo** — evita borrados por
  terceros.
- **Edición**: se publica una versión nueva con `replaces: <idAnterior>` y se
  lapida la anterior, formando una cadena de versiones; al leer se sigue la
  cadena hasta la "punta".
- Las comprobaciones de autoría (`if (c.createdBy !== userId) throw`) se hacen en
  el modelo.

**Tipos de mensaje SSB** (campo `content.type`): los nativos SSB `post`, `vote`
(likes), `contact` (follow/block), `about` (perfil), y los **propios de Oasis**:
`votes`, `tombstone`, `task`, `event`, `calendar`, `transfer`, `tribe`,
`market`, `report`, `job`, `project`, `gameScore`, etc.

> **`main_models.js`** es el modelo núcleo: exporta `about`, `blob`, `friend`,
> `meta`, `post`, `vote`, `spreads`, `lifetime`. Su función `transform()`
> enriquece cada mensaje con metadatos (autor, avatar, votos, timestamps) antes
> de pasarlo a las vistas.

---

## 2.5 Patrón de una VISTA

Las vistas exportan funciones que reciben datos del modelo y devuelven HTML.
Patrón típico:

```js
const { div, h2, p, section, button, form } = require("../server/node_modules/hyperaxe");
const { template, i18n, userLink } = require('./main_views');

exports.agendaView = async (data, filter) =>
  template(i18n.agendaTitle, section( h2(i18n.agendaTitle), /* ... */ ));
```

**Layout común — `main_views.js`**:
- `template(titlePrefix, ...elements)` construye el documento completo:
  `html → head (title, CSS, favicon) → body (header, sidebar de navegación,
  main-content)`. Se exporta como `exports.template`.
- **Navegación**: helper `navLink({href, emoji, text, current})` genera cada
  `<li><a>`. `navGroup({id, emoji, title})` agrupa enlaces en secciones
  colapsables del menú lateral. Cada módulo tiene su `renderXxxLink()` que solo
  emite el enlace si el módulo está activo en config.
- **i18n**: `i18n` es un objeto compartido importado por todas las vistas.
  `setLanguage(lang)` lo muta in-place (11 idiomas:
  `en, es, fr, eu, de, it, pt, zh, ar, hi, ru` en
  `src/client/assets/translations/`). El middleware llama `setLanguage(...)` en
  cada petición. Las cadenas se referencian como `i18n.agendaTitle`, con
  fallback `i18n.x || 'Default'`. **Nunca texto literal en las vistas.**
- **Temas**: `const theme = currentConfig.themes.current || "Dark-SNH"` →
  `<link href="/assets/themes/${theme}.css">`.
- **Helpers reutilizables** exportados por `main_views`: `userLink`,
  `renderStateChip`, `renderVisibilityChip`, `renderSpreadButton`, `errorView`,
  `formatCarbon`, `markdown(input, mentions)`.

---

## 2.6 Configuración

**`src/configs/config-manager.js`** — gestor central de `oasis-config.json`:
- Si el archivo no existe, escribe un `defaultConfig`.
- `getConfig()` lee y parsea el JSON **en cada llamada** (sin caché → cambios en
  caliente).
- `saveConfig(newConfig)` reescribe el archivo.

Qué controla **`oasis-config.json`**:
- `themes.current` — tema CSS activo.
- `ux.current` — modo de interfaz (`"blocks"` o `"ainav"`).
- **`modules`** — ~50 flags `<nombre>Mod: "on"|"off"` que activan/desactivan cada
  módulo. En backend se comprueba con `checkMod(ctx, mod)` (líneas 170-177): si
  está `off`, redirige a `/modules`.
- `wallet`/`walletPub`, `ai.prompt`, `ssbLogStream.limit` (límite de mensajes
  leídos del log), `homePage`, `language`, `wish`, `pmVisibility`,
  `lanBroadcasting`.

**`src/configs/shared-state.js`** — estado en memoria (no persistente):
contador de inbox, huella de carbono, nº de peers online, valor eco, etc.
Lo actualiza el middleware periódico y lo consume `template`.

---

## 2.7 Tabla de módulos (modelo ↔ vista)

| Modelo | Vista | Notas |
|---|---|---|
| `main_models.js` | `main_views.js` | Núcleo: layout, i18n, temas, about/blob/friend |
| `activity_model` | `activity_view` | Feed de actividad |
| `agenda_model` | `agenda_view` | Agrega tasks/events/transfers/market/... |
| `audios_model` | `audio_view` | |
| `banking_model` | `banking_views` | ECOin, epochs |
| `blockchain_model` | `blockchain_view` | |
| `bookmarking_model` | `bookmark_view` | |
| `calendars_model` | `calendars_view` | Cifrado |
| `chats_model` | `chats_view` | Cifrado |
| `cipher_model` | `cipher_view` | |
| `courts_model` | `courts_view` | |
| `cv_model` | `cv_view` | |
| `documents_model` | `document_view` | |
| `events_model` | `event_view` | |
| `favorites_model` | `favorites_view` | Agrega favoritos |
| `feed_model` | `feed_view` | |
| `forum_model` | `forum_view` | Cifrado |
| **`games_model`** | **`games_view`** | Hall of fame + shell de juegos |
| `images_model` | `image_view` | |
| `inhabitants_model` | `inhabitants_view` | Perfiles |
| `jobs_model` | `jobs_view` | |
| `larp_model` | `larp_view` | |
| `legacy_model` | `legacy_view` | |
| `logs_model` | `logs_view` | |
| `maps_model` | `maps_view` | Cifrado |
| `market_model` | `market_view` | |
| `melody_model` | `melody_view` | |
| `opinions_model` | `opinions_view` | |
| `pads_model` | `pads_view` | Cifrado |
| `parliament_model` | `parliament_view` | |
| `pixelia_model` | `pixelia_view` | |
| `pm_model` | `pm_view` | Mensajería privada |
| `projects_model` | `projects_view` | |
| `reports_model` | `report_view` | |
| `search_model` | `search_view` | |
| `shops_model` | `shops_view` | |
| `stats_model` | `stats_view` | |
| `tags_model` | `tags_view` | |
| `tasks_model` | `task_view` | |
| `torrents_model` | `torrents_view` | |
| `transfers_model` | `transfer_view` | |
| `trending_model` | `trending_view` | |
| `tribes_model` + `tribes_content_model` | `tribes_view` | Cifrado |
| `videos_model` | `video_view` | |
| `votes_model` | `vote_view` | |
| `wallet_model` | `wallet_view` | |
| (sin modelo) | `AI_view` | Lógica en `src/AI/*` |
| `crypto.js`/`tribe_crypto.js` | — | Cifrado simétrico por dominio |

---

## 2.8 Mapa de dependencias entre archivos

**`backend.js` → modelos** (líneas 541-602): importa `main_models` y **todos**
los `*_model.js` como factories invocadas con `{ cooler, isPublic, ...deps }`.
Hay **inyección de dependencias entre modelos**: p.ej. `agenda_model` recibe
`{calendarsModel, eventsModel, tasksModel, marketModel, jobsModel,
projectsModel}`; `favorites_model` recibe casi todos los modelos de contenido.

**`backend.js` → vistas** (líneas ~1162-1216 + `require` inline): importa las
funciones de render de cada `*_view.js`. Algunas se cargan diferidas dentro de
los handlers (`require('../views/...')`).

**Modelos → importan**: `pull-stream`, `moment`, `./tombstone_validator`,
`../configs/config-manager`, helpers, y reciben `cooler` por parámetro (no
importan `gui` directamente). `tombstone_validator.js` no tiene dependencias
(función pura).

**Vistas → importan**: hyperaxe, `{template, i18n, userLink, ...}` de
`main_views`, `moment`, y a veces `../server/SSB_server` para obtener
`config.keys.id` (el feedId propio).

**Estado compartido**: `config-manager.js` (lee/escribe `oasis-config.json`) y
`shared-state.js` (memoria) son importados por backend, modelos y vistas; son
los puntos de coordinación transversal. `nameCache.js` (un Map en memoria) lo
alimenta `main_models` y lo consume `main_views`.

Siguiente: **[03-modulos-y-juegos.md](03-modulos-y-juegos.md)**.
