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).
-dejecuta en segundo plano y--buildreconstruye la imagen — pasa siempre--builddespué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-datapersiste 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 alcanzarhttp://<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 comoAuthorization: Bearer <token>. - Ejecuta solo el backend durante el desarrollo con
npm run dev:server(recarga automática), onpm starten 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
- Frontend (Vite): http://localhost:5173 (proxifica
/api,/sound,/sfx,/docsa :4000) - API (Express): http://localhost:4000
- Docs: http://localhost:4000/docs (también accesible desde el icono ? bajo el interruptor de sonido en la app)
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
Crea un archivo JSON en
server/locales/llamado<code>.json, p. ej.de.json:{ "name": "Deutsch", "strings": { "app.title": "Blase", "app.tagline": "…" } }Eso es todo — el idioma aparece automáticamente en Perfil → Idioma. Los archivos se sirven mediante
GET /api/localesyGET /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
- Coloca un archivo de audio (
.wav,.mp3,.ogg,.m4a,.flac,.aac) enserver/public/sound/. - 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:
- Coloca un archivo de audio (
.wav,.mp3,.ogg,.m4a,.flac,.aac) enserver/public/sfx/. - 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/passwordGET /api/auth/sso/login,GET /api/auth/sso/callback
Autenticados (Authorization: Bearer <token>), limitados al usuario que ha iniciado sesión:
GET/PUT /api/profileGET/POST /api/priorities,PUT/DELETE /api/priorities/:idGET/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 delPOST{ title, category, type, description }—typeesfeatureobug,categoryes 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; incluyelikes,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/usersGET /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:
- Define
JWT_SECRETcon 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. - 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.
- Ejecuta con
NODE_ENV=productionpara que el servidor emita la advertencia de secreto ausente y cualquier comprobación exclusiva de producción. - 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_SECRETy 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.
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).
- URL de redirect / callback:
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/ |
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 usadocker 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 concrypto.randomBytes(18)y almacenada como un valor hasheable eninvite_codes; - de un solo uso — al consumirlo se marca
used_at/used_byy 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 offsolo 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
openssldirectamente ahash-passwordcrearí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íneaecho "$PW") debe mantenerse en secreto.docker-compose /
.env: defineADMIN_USERNAMEjunto conADMIN_PASSWORD(texto plano) oADMIN_PASSWORD_HASH(salt:hashprecalculado) 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.adminyblocked.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.
userslista cada cuenta conlast_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).--jsontambién incluye los nombres completos desteps_currenty el título/tipo de la ola actual.statsagrega 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(cuandoJWT_SECRETno está definido). - Motor: SQLite mediante el
node:sqliteintegrado 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.gitignorey 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/.datapara 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
stepsytasksrespectivamente — 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 EXISTSpara instalaciones nuevas.ALTER TABLE ADD COLUMN(protegido porPRAGMA table_info) para bases de datos existentes.- Un único
PRAGMA user_versionmantiene 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 montadobubble-data) entre actualizaciones — las migraciones aditivas significan que los datos antiguos siguen funcionando con el código nuevo.