MiMo Docs
🌐
Technical User manual ← App

MiMo (Mind & Motion) — Documentación técnica

Una aplicación de una sola página, estética y ligera, construida alrededor de una Burbuja central que guarda todo lo que piensas (Mente) y todo aquello en lo que avanzas (Movimiento).

Para usuarios finales, consulta USER_MANUAL.md.

Mantén este documento (y USER_MANUAL.md) actualizado siempre que la aplicación cambie. Si añades una función, una flag, un endpoint o un ajuste, refléjalo aquí en el mismo commit.


Stack tecnológico

Capa Tecnología
Backend Node.js 22+ (ESM), Express 4, node:sqlite integrado (SQLite)
Auth hash de contraseña con scrypt + JWT firmado con HMAC (sin librerías externas)
SSO openid-client (OIDC de Authentik, con soporte PKCE)
Frontend React 19 + Vite 7, CSS propio (sin framework de UI)
Dependencias de runtime express, cors, dotenv, openid-client, react, react-dom
Dependencias de desarrollo vite, @vitejs/plugin-react, concurrently

No hay ningún servidor de base de datos externo: los datos viven en un único archivo SQLite local (.data/bubble.db) mediante el node:sqlite integrado de Node.


Referencia de comandos

Comando Qué hace
npm run dev Ejecuta la API (node --watch) y el servidor de desarrollo de Vite juntos.
npm run dev:server Ejecuta solo la API con recarga automática.
npm run dev:web Ejecuta solo Vite (--host para exponerlo en la red).
npm run build Compila el cliente en ./dist.
npm start Ejecuta la API en modo producción (sirve ./dist).
node scripts/generate-ambient.mjs Regenera los archivos de sonido ambiental integrados.
node scripts/generate-sfx.mjs Regenera los archivos de efectos de sonido de la UI integrados.
node scripts/admin.mjs … Gestiona el registro y los códigos de invitación (ver Administración).

Producción

Despliegue (con Docker)

Se incluye un Dockerfile multi-etapa (imagen base node:24-alpine: la versión de npm 10.9 incluida en Node 22 tiene un bug con npm ci, por lo que la imagen usa Node 24/npm 11).

La forma más sencilla — Docker Compose (se incluye un docker-compose.yml):

JWT_SECRET=$(openssl rand -hex 32) APP_URL=https://mimo.2kaya.me docker compose up -d --build

APP_URL es tu URL web pública (usada para los redirects de SSO); JWT_SECRET firma los tokens de inicio de sesión: configúrala una vez y mantenla estable. La base de datos SQLite persiste en un volumen con nombre (mimo-data).

-d ejecuta en segundo plano y --build reconstruye la imagen — pasa siempre --build después de un cambio de código, de lo contrario Compose seguirá sirviendo la imagen antigua. Configura los demás secretos de la misma forma (OIDC_* y, opcionalmente, ADMIN_USERNAME + ADMIN_PASSWORD[_HASH] para crear un administrador en el primer arranque — ver Cuentas de administrador y .env.example).

O ejecuta la imagen directamente:

docker build -t noahs-bubble .
docker run -p 4000:4000 -v bubble-data:/app/.data noahs-bubble
  • App: http://localhost:4000
  • El volumen con nombre bubble-data persiste la base de datos SQLite entre reinicios.
  • Pasa la configuración mediante variables de entorno, p. ej. -e JWT_SECRET=... -e AUTHENTIK_ISSUER=....

Despliegue (sin Docker)

npm install
npm run build     # builds the client into ./dist
npm start         # serves ./dist + API on http://localhost:4000

En el primer arranque el servidor crea .data/ (base de datos SQLite + secreto JWT).


Dimensionamiento del servidor

La aplicación es un único proceso Node.js respaldado por SQLite (modo WAL), con autenticación JWT sin estado y servido de archivos local — lo que es exactamente adecuado para un despliegue personal/pequeño. Requisitos aproximados para distintas escalas:

Usuarios vCPU RAM Disco Notas
10 1 512 MB 1 GB Cualquier VPS pequeño (~5 €/mes). SQLite es instantáneo.
100 1–2 1 GB 5–10 GB Sigue siendo trivial. Añade copias de seguridad diarias de .data/.
10 000 2–4 2–4 GB 20–50 GB SQLite es suficiente (mucha lectura, WAL). Pon nginx/Caddy + HTTPS por delante, activa gzip y añade monitorización.
1 000 000 8+ (×N) 8 GB ×N almacenamiento de objetos El escritor único de SQLite se convierte en el cuello de botella — tendrías que migrar a PostgreSQL, ejecutar varias instancias de la app detrás de un balanceador de carga (el JWT ya es sin estado), servir los sonidos/sfx desde almacenamiento de objetos + CDN y añadir rate-limiting/Redis. Es una refactorización real (abstracción de BD + pooling), no un cambio de configuración.

El salto a PostgreSQL es el principal cambio futuro para una escala muy grande; el esquema y la API ya son lo bastante simples como para portarlos. Hasta entonces, mantén .data/ respaldado y el único nodo SQLite detrás de un proxy inverso con HTTPS.


Servir la API para una app móvil

El backend es una API HTTP simple, por lo que un futuro cliente móvil (p. ej. un APK de Android) puede conectarse a ella directamente — la misma API que usa la interfaz web.

  • El servidor Express escucha en todas las interfaces en :4000 (el host de enlace no está restringido), de modo que un teléfono/emulador en la misma red puede alcanzar http://<server-ip>:4000/api/….
  • CORS está abierto (app.use(cors())), por lo que se permiten peticiones de cualquier origen.
  • La autenticación se basa en tokens: llama a POST /api/auth/login (o a /api/auth/register, o al flujo SSO) para obtener un JWT y envíalo como Authorization: Bearer <token>.
  • Ejecuta solo el backend durante el desarrollo con npm run dev:server (recarga automática), o npm start en producción.

El cliente móvil puede reutilizar todos los endpoints listados en Resumen de la API.


Configuración

Copia .env.example a .env. Todos los valores son opcionales salvo donde se indique.

Variable Propósito
PORT Puerto de la API (por defecto 4000).
DATA_DIR Dónde vive la base de datos SQLite (por defecto .data/). Usa una ruta temporal al probar para no tocar los datos reales de los usuarios.
APP_URL URL base pública, usada para los redirects de SSO (por defecto en desarrollo http://localhost:5173).
JWT_SECRET Secreto de firma de los tokens de inicio de sesión. Si no se define, se persiste un secreto aleatorio en .data/.jwt-secret.
OIDC_ISSUER URL de descubrimiento del proveedor OIDC (habilita SSO). AUTHENTIK_ISSUER funciona como alias.
OIDC_CLIENT_ID ID de cliente OIDC (alias AUTHENTIK_CLIENT_ID).
OIDC_CLIENT_SECRET Secreto del cliente OIDC — omítelo para un cliente público → PKCE (alias AUTHENTIK_CLIENT_SECRET).
OIDC_REDIRECT_URI Opcional; por defecto <APP_URL>/api/auth/sso/callback (alias AUTHENTIK_REDIRECT_URI).

Desarrollo

Inicio rápido

Requisitos: Node.js 22+ (para node:sqlite).

npm install
npm run dev

Dependencias

Tiempo de ejecución (dependencies)

  • express — servidor HTTP + enrutado + servido de estáticos.
  • cors — cabeceras CORS para el servidor de desarrollo.
  • dotenv — carga .env.
  • openid-client — descubrimiento OIDC de Authentik + flujo de código.
  • react / react-dom — la interfaz del cliente (empaquetada por Vite).

Desarrollo (devDependencies)

  • vite + @vitejs/plugin-react — build/servidor de desarrollo.
  • concurrently — ejecuta la API y Vite juntos.

Intencionadamente no hay ORM, ni bcrypt, ni jsonwebtoken: el hash de contraseñas usa crypto.scrypt de Node, los tokens se firman con HMAC-SHA256 y la base de datos es el node:sqlite integrado de Node. Esto mantiene mínima la huella de dependencias de runtime.


Añadir una traducción

  1. Crea un archivo JSON en server/locales/ llamado <code>.json, p. ej. de.json:

    {
      "name": "Deutsch",
      "strings": { "app.title": "Blase", "app.tagline": "…" }
    }
    
  2. Eso es todo — el idioma aparece automáticamente en Perfil → Idioma. Los archivos se sirven mediante GET /api/locales y GET /api/locales/:code.

Eliminar un archivo elimina el idioma de la lista. No se requieren cambios de código.


Añadir un sonido de fondo

  1. Coloca un archivo de audio (.wav, .mp3, .ogg, .m4a, .flac, .aac) en server/public/sound/.
  2. Aparece automáticamente en Perfil → Sonido ambiental → Sonido (el nombre del archivo sin la extensión se convierte en su etiqueta, p. ej. ocean-waves.mp3 → "Ocean Waves").

Ejemplos integrados: aurora.wav, tide.wav, ember.wav (regenerables con node scripts/generate-ambient.mjs).


Añadir un efecto de sonido (sonidos de la UI)

Los sonidos de la UI (p. ej. el "whoosh" al arrastrar la burbuja) funcionan igual:

  1. Coloca un archivo de audio (.wav, .mp3, .ogg, .m4a, .flac, .aac) en server/public/sfx/.
  2. Aparece automáticamente en Perfil → Sonido ambiental → Efecto de sonido, donde cada usuario puede elegir su preferido.

Ejemplos integrados: whoosh.wav, chime.wav, blip.wav (regenerables con node scripts/generate-sfx.mjs).


Resumen de la API

Públicos:

  • GET /api/config — lista de sonidos, SSO habilitado, límite de pasos, registro abierto/cerrado.
  • GET /api/locales, GET /api/locales/:code — catálogos de traducciones.
  • GET /api/health.
  • GET /api/charity — causa benéfica del mes + días restantes (placeholder; se muestra una vez al mes).

Autenticación:

  • POST /api/auth/register { username, password, name?, inviteCode? }
  • POST /api/auth/login { username, password }
  • GET /api/auth/me, POST /api/auth/password
  • GET /api/auth/sso/login, GET /api/auth/sso/callback

Autenticados (Authorization: Bearer <token>), limitados al usuario que ha iniciado sesión:

  • GET/PUT /api/profile
  • GET/POST /api/priorities, PUT/DELETE /api/priorities/:id
  • GET/POST /api/categories, PUT/DELETE /api/categories/:id (etiquetas de tarea, máx. 5)
  • GET/POST /api/waves, PUT/DELETE /api/waves/:id (?archived=1)
  • GET/POST /api/steps, PUT/DELETE /api/steps/:id (los pasos incluyen tareas)
  • POST /api/tasks, PUT/DELETE /api/tasks/:id (priority_id, category_id)
  • GET /api/archive (olas y tareas archivadas combinadas)
  • GET /api/insights (todas las reflexiones diarias), GET /api/insights/today?date=YYYY-MM-DD, POST /api/insights { date, answer, elaboration }
  • GET/POST /api/suggestions (feedback de la app; cuerpo del POST { title, category, type, description } — type es feature o bug, category es un slug de /api/forum/meta)
  • GET/POST /api/friends, PUT/DELETE /api/friends/:id (añadir por { code }, ocultar burbuja, eliminar)
  • GET /api/friends/:id/peek (vistazo de solo lectura a la burbuja de un amigo mutuo)

Foro (autenticado — requiere inicio de sesión):

  • GET /api/forum/meta — listas de categoría y tipo (traducidas al idioma del usuario).
  • GET /api/forum/suggestions?page=1&limit=10 — publicaciones paginadas, ordenadas fijadas → más gustadas → más recientes; incluye likes, comments, liked_by_me.
  • POST /api/forum/suggestions { title, category, type, description } — nueva publicación.
  • GET /api/forum/suggestions/:id — una publicación + sus comentarios.
  • POST /api/forum/suggestions/:id/like — alternar un «me gusta».
  • POST /api/forum/suggestions/:id/comments { body } — añadir un comentario.
  • PATCH /api/forum/suggestions/:id { pinned } — solo administradores — fijar/desfijar.

Administración (requiere un usuario is_admin — ver Cuentas de administrador):

  • GET /api/admin/users
  • GET /api/admin/invites, POST /api/admin/invites { count }
  • DELETE /api/admin/invites/:code

Preguntas de reflexión diaria

Las preguntas viven en server/insights.js — añade nuevas al array INSIGHT_QUESTIONS de ahí. Cuando hay más de una, questionFor(userId, date) elige una de forma determinista por usuario + día (efectivamente aleatoria entre días/usuarios, pero estable durante todo el día). A cada usuario se le pregunta una vez por día local a su insight_hour (configurado en Perfil → Reflexión diaria).


Estructura del proyecto

server/            Express API (ESM)
  index.js         app entry + static serving
  db.js            SQLite schema + additive migrations
  auth.js          scrypt hashing + HMAC-signed tokens
  admin.js         registration gate + invite-code helpers
  oidc.js          Authentik OIDC
  routes.js        profile / priorities / waves / steps / tasks / archive / insights
  insights.js      daily insight questions
  charity.js       monthly charity (placeholder)
  sounds.js        scans server/public/sound/
  i18n.js          scans server/locales/
  locales/         translation files (<code>.json)
  public/sound/    ambient audio files
client/            React + Vite frontend
  src/
    context.jsx    auth + data store
    fonts.js       available display fonts
    sfx.js         Web-Audio sound effects
    components/    Background, Bubble, Stage, WaveNode/Editor/Create,
                   StepColumn/Expanded, TaskCard/Expanded, Archive, Profile, …
scripts/           generate-ambient.mjs, generate-sfx.mjs, admin.mjs (CLI)

Consejos de desarrollo

  • Mantén ambos README actualizados en el mismo commit que cualquier cambio de comportamiento.
  • Nunca elimines columnas/tablas en una migración — añade nuevas con valores por defecto seguros; los datos existentes de los usuarios siempre deben sobrevivir a una actualización.
  • El frontend está pensado primero para escritorio, pero es responsive en móviles (diseño apilado).
  • Las funciones de arrastre/desplazamiento espacial viven en Stage.jsx/Bubble.jsx; el experimento de «arrastrar la burbuja para enfocar Mente/Movimiento» está intencionadamente aislado para poder revertirse de forma independiente.

Seguridad

Seguridad en producción

Las sesiones son tokens firmados con HMAC. La clave de firma es un secreto aleatorio que se genera una vez y se persiste en .data/.jwt-secret, de modo que los inicios de sesión siguen siendo válidos entre reinicios (sin volver a iniciar sesión tras cada despliegue).

Antes de exponer la aplicación públicamente:

  1. Define JWT_SECRET con un valor aleatorio largo (p. ej. openssl rand -hex 32). Si no se define, el servidor sigue funcionando pero registra una advertencia en modo producción y depende del secreto persistido — defínelo explícitamente para que la clave de firma sea deliberada y estable.
  2. Sirve sobre HTTPS. Pon la aplicación detrás de un proxy inverso (Caddy, nginx, Traefik) que termine TLS; el JWT viaja en el localStorage del navegador y nunca debería cruzar la red en texto claro.
  3. Ejecuta con NODE_ENV=production para que el servidor emita la advertencia de secreto ausente y cualquier comprobación exclusiva de producción.
  4. Cierra el registro una vez que existan tus usuarios y reparte códigos de invitación en su lugar — ver Administración. Mantén JWT_SECRET y el directorio .data/ fuera del control de versiones.

Inicio de sesión único (OIDC)

La app usa openid-client, por lo que cualquier proveedor compatible con OIDC funciona: Authentik, Google, Codeberg, Keycloak, Auth0, Zitadel y otros.

  1. Crea una aplicación OIDC en tu proveedor con:

    • URL de redirect / callback: <APP_URL>/api/auth/sso/callback
    • Scopes: openid profile email
    • Un cliente confidencial (con secreto) o un cliente público (sin secreto → PKCE).
  2. Configura en .env:

    OIDC_ISSUER=https://accounts.google.com
    OIDC_CLIENT_ID=…
    OIDC_CLIENT_SECRET=…
    # OIDC_REDIRECT_URI defaults to <APP_URL>/api/auth/sso/callback
    

URLs de issuer (descubrimiento) comunes:

Proveedor OIDC_ISSUER
Authentik https://authentik.example/application/o/bubble/
Google https://accounts.google.com
Codeberg (Forgejo) https://codeberg.org
Keycloak https://keycloak.example/realms/myrealm
Auth0 https://YOUR_DOMAIN.auth0.com
Zitadel https://zitadel.example

Los usuarios de SSO se emparejan por su sub; el primer inicio de sesión crea una cuenta local automáticamente. Los antiguos nombres de variable AUTHENTIK_* siguen funcionando como alias.

/api/auth/sso/login acepta un ?redirect=<url> opcional (http(s) o un esquema de app personalizado como mimo://) para que un wrapper de escritorio pueda recibir el ?token=… final — de lo contrario, por defecto usa APP_URL (la app web).


Administración

El registro se gestiona desde la línea de comandos con la herramienta de administración (node scripts/admin.mjs …, ejecutada desde la raíz del proyecto — no requiere servidor, habla directamente con la base de datos SQLite):

Hay un atajo más corto: ./admin-manager.sh … — p. ej. ./admin-manager.sh make-admin admin --hash '<hash>' o ./admin-manager.sh status. También se copia en la imagen de Docker, así que en producción usa docker compose exec mimo ./admin-manager.sh ….

Comando Qué hace
node scripts/admin.mjs status Muestra si el registro está abierto/cerrado y el uso de los códigos de invitación.
node scripts/admin.mjs registration on Permite que cualquiera se registre (por defecto).
node scripts/admin.mjs registration off Exige un código de invitación válido para registrarse.
node scripts/admin.mjs invite [count] Genera count (por defecto 1) códigos de invitación.
node scripts/admin.mjs invites Lista todos los códigos de invitación y si cada uno está usado.
node scripts/admin.mjs revoke <code> Elimina un código de invitación (usado o no).
node scripts/admin.mjs hash-password Imprime un salt:hash scrypt (lee la contraseña de stdin).
node scripts/admin.mjs make-admin <user> [pw] Concede derechos de administrador; crea el usuario si se da una contraseña.
node scripts/admin.mjs make-admin <user> --hash <salt:hash> Concede administrador usando un hash precalculado (sin texto plano).
node scripts/admin.mjs users [--json] Analítica por usuario: último inicio de sesión, olas, pasos, tareas.
node scripts/admin.mjs stats [--json] Estadísticas agregadas: medianas/medias (tareas por paso, etc.).

Cuando el registro está cerrado (la fila registration_enabled de la tabla settings es '0'), el formulario de registro muestra un campo adicional de código de invitación. Un código es:

  • una cadena de 24 caracteres segura para URL (p. ej. xYz…), generada con crypto.randomBytes(18) y almacenada como un valor hasheable en invite_codes;
  • de un solo uso — al consumirlo se marca used_at/used_by y no puede usarse de nuevo;
  • revocable en cualquier momento con revoke, incluso antes de usarse.

La API refleja el estado actual en GET /api/config (registrationEnabled), para que el cliente sepa si debe mostrar el campo de invitación. Los códigos se comprueban en el servidor en POST /api/auth/register (inviteCode); un registro cerrado rechaza los registros con 403 a menos que el código sea válido y no esté usado.

El SSO es independiente de este control: registration off solo bloquea el registro basado en contraseña, no el primer inicio de sesión OIDC.

Cuentas de administrador

Los derechos de administrador son una flag en la fila del usuario (users.is_admin, añadida por una migración aditiva). Dos formas de concederla:

  • CLI (sin servidor): node scripts/admin.mjs make-admin <username> [password] — promueve una cuenta existente o crea una nueva cuando se da una contraseña (6+ caracteres).

  • CLI, sin texto plano en el historial del shell: genera primero un hash y luego pásalo a make-admin:

    printf 'your-password' | node scripts/admin.mjs hash-password   # prints salt:hash
    node scripts/admin.mjs make-admin <username> --hash '<salt:hash>'
    

    Para usar una contraseña generada aleatoriamente, imprímela una vez para poder guardarla (canalizar openssl directamente a hash-password crearía un administrador cuya contraseña nunca verías y con la que no podrías iniciar sesión):

    # Generate a random password, show it once (copy & save it), and hash it:
    PW="$(openssl rand -base64 32)"; echo "$PW"; printf '%s' "$PW" | node scripts/admin.mjs hash-password
    # …then create the admin with the printed hash:
    node scripts/admin.mjs make-admin admin --hash '<salt:hash>'
    

    El hash no se puede revertir, por lo que es seguro pegarlo en el shell/make-admin; solo el texto plano (la línea echo "$PW") debe mantenerse en secreto.

  • docker-compose / .env: define ADMIN_USERNAME junto con ADMIN_PASSWORD (texto plano) o ADMIN_PASSWORD_HASH (salt:hash precalculado) y el servidor creará/promoverá esa cuenta al arrancar (ver .env.example).

Cambiar la contraseña de un administrador: make-admin crea el usuario si no existe; si sí existe, pasar una contraseña/--hash también la actualiza, por lo que el mismo comando sirve como restablecimiento de contraseña del administrador:

PW="$(openssl rand -base64 32)"; echo "$PW"; printf '%s' "$PW" | node scripts/admin.mjs hash-password
node scripts/admin.mjs make-admin admin --hash '<salt:hash>'   # existing admin → password updated

Los administradores también pueden cambiar su propia contraseña en la app (Perfil → contraseña).

La vista de administrador es una sección de la página de inicio (MiMo-Front) que aparece solo después de que un administrador inicie sesión: crear/revocar códigos de invitación y listar usuarios. El cliente solo oculta el botón a los no administradores — cada endpoint de administración se aplica en el servidor mediante requireAdmin (JWT válido y is_admin), de modo que ningún cliente puede saltarse el control.

API de administración (solo auth + admin)

Viven bajo /api/admin/* y se rechazan con 403 para los no administradores:

  • GET /api/admin/users — analítica por usuario (getUsersAnalytics), incl. admin y blocked.
  • PATCH /api/admin/users/:id { admin?, blocked? } — cambia el rol de un usuario (admin sí/no) o bloquéalo/desbloquéalo. Los administradores no pueden modificarse a sí mismos.
  • DELETE /api/admin/users/:id — elimina un usuario + todos sus datos (en cascada). Los administradores no pueden eliminarse a sí mismos.
  • GET /api/admin/invites — lista los códigos de invitación + estado usado/creado.
  • POST /api/admin/invites { count } — genera 1–100 códigos de invitación.
  • DELETE /api/admin/invites/:code — revoca un código de invitación.

Los usuarios bloqueados son rechazados por requireAuth (y por el inicio de sesión con contraseña) con 403, de modo que el bloqueo surte efecto de inmediato, incluso para tokens ya emitidos.

Analítica (CLI de solo lectura)

Los comandos users y stats son de solo lectura y se exportan desde server/admin.js (getUsersAnalytics, getDashboardStats). Las medianas/medias agregadas de stats deliberadamente no se exponen por HTTP — solo la lista por usuario y la gestión de invitaciones están disponibles para los administradores (arriba), de modo que los números agregados del dashboard quedan fuera del servidor MiMo orientado al usuario.

  • users lista cada cuenta con last_login_at, olas activas/archivadas, pasos actuales vs totales, tareas activas/hechas/archivadas, prioridades, flag de SSO y la ola actual (la ola activa creada más recientemente). --json también incluye los nombres completos de steps_current y el título/tipo de la ola actual.
  • stats agrega sobre toda la base de datos: crecimiento de usuarios (7d/30d), SSO vs contraseña, cuentas inactivas, medianas y medias de olas/pasos/tareas por usuario, tasa de finalización de tareas, mediana de tareas por paso y uso de códigos de invitación.

last_login_at se marca en cada inicio de sesión exitoso (contraseña, registro o SSO) mediante touchLastLogin en server/auth.js; la columna se añade mediante una migración aditiva, de modo que las cuentas existentes simplemente empiezan con NULL hasta su siguiente inicio de sesión.


Base de datos

  • Archivo: .data/bubble.db (ignorado por git), más un secreto JWT generado en .data/.jwt-secret (cuando JWT_SECRET no está definido).
  • Motor: SQLite mediante el node:sqlite integrado de Node (modo WAL, claves foráneas activadas).

Cómo funciona el archivo SQLite

bubble.db es un único archivo de base de datos SQLite. Como el modo WAL está activado, también verás bubble.db-wal y bubble.db-shm junto a él — son el log de escritura anticipada (write-ahead) y el índice de memoria compartida que se usan mientras el servidor está en marcha; en un apagado limpio se pliegan de nuevo en bubble.db. Todo el contenido de los usuarios vive en estos archivos, así que:

  • Reiniciar el servidor (o reconstruir la app) lo conserva todo, siempre que los archivos no se eliminen.
  • .data/ está en .gitignore y en .dockerignore, de modo que nunca se commitea ni se hornea en la imagen de Docker — la app nunca se distribuye con tus datos.
  • En Docker, monta un volumen en /app/.data para persistir entre reconstrucciones del contenedor.

Para probar la persistencia en desarrollo: crea algunas olas/tareas, detén npm run dev y vuelve a iniciarlo — todo sigue ahí. Para mantener una base de datos de prueba desechable separada de los datos reales, define DATA_DIR=/tmp/....

Esquema

Terminología: los términos orientados al usuario son Espacios (las columnas) y Pasos (los elementos dentro de ellas). En la base de datos estos son las tablas steps y tasks respectivamente — los nombres de tabla/columna de abajo son los nombres en el código.

users (
  id, username (unique), email, password_hash, name, age, pronouns, wishes,
  language, sound_muted, sfx_muted, sound_id, sfx_id, font_id, steps_mode,
  authentik_sub, created_at, last_login_at (nullable), insight_hour (0-23, default 9),
  friend_code (unique, generated)
)
priorities ( id, user_id → users, name, color, position )
categories ( id, user_id → users, name, color, position )  -- step tags (max 5)
waves (
  id, user_id → users, title, description,
  type ('bullet'|'checklist'|'range'), content (JSON), priority_id → priorities,
  archived, archived_at, position, created_at
)
steps ( id, user_id → users, name, position, workflow (0=custom, 1=intended) )  -- "Spaces"
tasks (  -- "Steps"
  id, user_id → users, step_id → steps (nullable once archived), title,
  type ('task'|'checklist'), subtasks (JSON), priority_id → priorities,
  category_id → categories (nullable), completed, archived, archived_at,
  step_label, position, created_at
)
insights (
  id, user_id → users, date ('YYYY-MM-DD'), question, answer (1-2 words),
  elaboration, answered_at; unique (user_id, date)
)
suggestions ( id, user_id → users, title, category, description, created_at )
friendships ( id, user_id → users, friend_id → users, hidden, created_at, unique(user_id, friend_id) )

Formas de contenido:

  • ola bullet → { items: string[] }
  • ola checklist → { items: [{ text, done }] }
  • ola range → { min, max, value, history: [{ date, value, max }] }
  • tarea subtasks → [{ text, done }]

Migraciones y persistencia de datos

Todas las migraciones son aditivas. Los cambios de esquema se ejecutan en server/db.js:

  • CREATE TABLE IF NOT EXISTS para instalaciones nuevas.
  • ALTER TABLE ADD COLUMN (protegido por PRAGMA table_info) para bases de datos existentes.
  • Un único PRAGMA user_version mantiene consistentes los valores por defecto heredados.

Nunca elimines .data/. Contiene todo el contenido de los usuarios. Los despliegues deben mantener intacto el archivo de base de datos (en Docker, el volumen montado bubble-data) entre actualizaciones — las migraciones aditivas significan que los datos antiguos siguen funcionando con el código nuevo.