Producto propio · Next.js y Supabase
Una app de seguimiento personal, y el porqué de cada decisión
Cierra el día —hábitos, cuerpo, sueño, comida, gastos— en un formulario y un botón, y lo devuelve leído en vez de en crudo. Hay un demo abierto con las cinco pantallas de verdad y los datos de una persona que no existe.
Repositorio privado · el demo es la superficie pública
Demo público y app en producción · el alta de cuentas está escrita y todavía cerrada
- 5pantallas reales en el demo, no maquetas
las mismas de la app, corriendo sobre datos de alguien inventado
- 0consultas a la base desde el demo
medido en el navegador: cero peticiones y cero envíos en las seis rutas
- 526pruebas automáticas en ocho puertas de CI
tipos, formato, lint, pruebas, build, fuga en el bundle, dependencias y RLS
- 34tablas con política de fila por usuario
y las claves primarias reconstruidas, que era lo que de verdad bloqueaba
El demo
Las cinco pantallas de verdad, con los datos de alguien que no existe
Una app cuyos datos son las finanzas y el cuerpo de una persona no se enseña con capturas. El demo corre las mismas cinco pantallas —no copias— y las mismas funciones de cálculo sobre ciento veinte días generados a partir de una semilla: las mismas cifras en cada recarga, así que una captura no caduca. Los ciento veinte días terminan hoy, de modo que el demo tampoco envejece.
El demo no consulta la base de datos. Nunca.
No es una promesa escrita en un documento: una prueba recorre el grafo de importaciones que existe en tiempo de ejecución desde el módulo del demo y falla si alguno de los módulos alcanzables toca el cliente de la base. Es seguro por construcción y no por permiso — aunque cayera la guardia de rutas, no habría consulta que servir. Comprobado además en el navegador, recorriendo las seis rutas: cero peticiones a la base y cero envíos.
Lo que se ve
Capturas tomadas del propio demo, con su franja impresa.




Lo que la persona del demo tiene, y para qué
- 1Tres días en pausa por un viaje, para enseñar que un hueco declarado no es un fallo.
- 2Un hábito graduado con su nota: la salida explícita de una métrica que ya cumplió.
- 3Peso y cintura medidos tres veces por semana, para que las series tengan huecos que nadie interpola.
- 4Una compra a doce cuotas y un cobro en cinco, que es lo que separa el gasto corriente del diferido.
- 5Un bolsillo ajeno, etiquetado como custodia de terceros y fuera de toda suma de patrimonio.
Decisiones
Seis decisiones, cada una contra un modo concreto de abandonar
La mitad de la gente deja una app de seguimiento en el primer mes, y uno de cada ocho abandonos es de alguien que ya aprendió lo que necesitaba. Esas dos cifras son el punto de partida: cada decisión responde a una forma documentada de fallar, no a una preferencia estética.
Un hueco y un fallo no son lo mismo
Un día sin marcar sale del denominador; uno marcado en no cuenta como cero; y un día declarado en pausa —un viaje, una gripe— no cuenta ni a favor ni en contra. Sin esa distinción, dos semanas de vida normal se leen como catorce fracasos.
Graduarse es un final feliz
Un hábito se puede graduar con una nota: deja de pedirse a diario, se queda en la historia y no hunde la adherencia del resto. Sale del denominador solo desde la fecha en que se gradúa, porque los días anteriores ya contaron y reescribirlos cambiaría un historial que fue verdad.
La referencia es tuya, no la del promedio
Cada categoría de gasto se compara contra su presupuesto si lo tiene y contra tu propia media si no, y la pantalla dice siempre de dónde salió el número. La curva de ritmo esperado no es una recta: sale de la forma que tuvieron tus meses ya cerrados, porque el arriendo no se paga en cuotas diarias.
El peso no es una meta
No hay barra de progreso hacia un peso objetivo en ninguna pantalla. La cintura sí la tiene, porque mide lo que de verdad se quería cambiar. Es la diferencia entre una app que acompaña y una que vigila.
Ningún dato inventado
Un campo vacío se muestra vacío y las series no se interpolan: una recta entre dos pesajes separados por dos semanas no es una tendencia. Cuando no hay días suficientes para afirmar algo, la app lo dice en vez de afirmarlo igual.
El color se midió, no se eligió
Cada color pasó por validación de contraste en los dos temas, y los seis de los hábitos llevan luminancias escalonadas para seguir distinguiéndose en escala de grises o con daltonismo. Los estados del calendario se separan por forma —relleno, trama, contorno—, que es lo único que sobrevive a una pantalla al sol.
Datos
Postgres con seguridad por fila, y las claves que el multiusuario destapó
Una política por fila, no por pantalla
Treinta y tres tablas y veintidós vistas en Postgres, cada una con su política y las vistas en modo invocador. El rol anónimo no lee nada: una prueba de humo recorre las cincuenta y cinco por la misma API que usa la app y exige que las cincuenta y cinco denieguen.
Lo que bloqueaba el multiusuario no era la seguridad
Eran las claves primarias. Veintidós tablas se identificaban sin el usuario: las medidas por fecha, los hábitos por fecha y nombre, las cuentas por nombre. La segunda persona que se pesara un martes recibía un error de clave duplicada, y una sola persona en todo el sistema podía tener una cuenta con el nombre de su banco.
Las claves foráneas llevan el usuario dentro
Sin él, un movimiento podía apuntar a la cuenta de otra persona y la seguridad por fila no lo impediría, porque una clave foránea se valida dentro del motor y no pasa por la política.
Un cero no es un dato
Vacío se guarda como nulo, nunca como cero, y un campo en blanco no borra lo que ya se midió ese día: las claves nulas se quitan del envío antes de escribir.
El módulo de los números
Un campo numérico del navegador rechaza la coma decimal y lee 10.500 como diez y medio, así que un gasto se guardaba dividido por mil sin que nada avisara. Todos los campos son de texto y pasan por un lector propio con sus reglas escritas y probadas; debajo de cada uno se muestra el valor exacto que se va a guardar, ya recortado a los decimales de la columna.
Las gráficas son SVG escrito a mano
Treinta líneas de aritmética en vez de una librería: se prueban con el resto del código sin montar un navegador, y el teléfono no baja veinte kilobytes para dibujar seis líneas.
Procesos
Ocho puertas antes de que un cambio llegue a producción
Cada empujón y cada propuesta de cambio pasa por la misma cadena, y cada puerta se juzga por su código de salida y no por lo que imprime. La cadena completa, corrida sobre la rama principal:
- 1TiposTypeScript estricto, sin un solo error
- 2FormatoPrettier en modo verificación
- 3LintESLint con la configuración de Next
- 4Pruebas526 pruebas en 38 ficheros
- 5Buildcompilación de producción completa
- 6Fuga en el bundle12 términos personales, ninguno en los 28 ficheros servidos
- 7Dependenciasauditoría sin vulnerabilidades altas
- 8Seguridad por fila55 de 55 comprobaciones deniegan al anónimo
Cuatro guardias contra una regresión concreta
Aislamiento del demo
Recorre el grafo de importaciones de valor desde el módulo del demo y falla si alguno de los módulos alcanzables llega al cliente de la base.
Escrituras del demo
Recorre los componentes de cliente y exige que todo el que importe una acción de servidor pregunte si está en modo demo, o quede exento con su razón escrita.
Claves de conflicto
Toda escritura con resolución de conflicto tiene que empezar por el usuario. Sin esa guardia, el paso a multiusuario rompía once guardados en silencio: la base impecable y la app inservible.
Datos personales en el artefacto
Corre después del build y sobre lo que se sirve, no sobre el código fuente: quien decide qué viaja al navegador es el empaquetador y no la importación. Está probado que detecta una fuga plantada.
Seguridad
Dos hallazgos que solo aparecen cuando se abre una puerta
El bundle también es una superficie
Abrir el demo no expone solo lo que el demo pinta: expone los ficheros de JavaScript de las pantallas, porque son los mismos. La lista real de hábitos vivía en un módulo de constantes compartido y el empaquetador metía el módulo entero en el envío al navegador; dos de esos nombres son datos de categoría especial. Se movieron a un módulo de solo servidor, con dos guardias detrás. No llegó a estar expuesto: se corrigió antes de fusionar.
Una escritura del demo podía llegar a datos reales, y no por la red
El borrador del formulario vive en el almacenamiento del propio teléfono con la fecha como clave, y el demo usa la fecha de hoy. Quien abriera el demo desde el mismo navegador con el que registra pisaba su borrador real del día, y sin ningún error visible. En modo demo el borrador ya no lee, ni guarda, ni limpia, ni encola.
Sin terceros, y con política de contenido por petición
Ni fuentes externas, ni analítica, ni scripts de nadie. La política de seguridad de contenido se emite con un número de un solo uso en cada petición, y la caché para trabajar sin conexión no guarda datos: solo estáticos y la pantalla de sin conexión. Cerrar sesión borra el almacenamiento local, que no está cifrado.
Estado
Qué está vivo y qué falta
Vivo hoy
- 1El demo es público y no pide cuenta: las cinco pantallas, con datos generados.
- 2La app corre en producción para su autor, detrás de sesión y con seguridad por fila.
- 3El esquema multiusuario está aplicado, y el aislamiento entre dos usuarios comprobado con un usuario desechable que se borró después.
Lo que falta para abrir las diez cuentas
- 1El alta está escrita y probada, y cerrada en dos sitios a la vez: una fila en la base y el interruptor de altas del panel de autenticación, que hoy sigue respondiendo que el registro está deshabilitado.
- 2El correo de confirmación necesita un servidor de envío propio: el que viene incluido está limitado por hora, y con diez altas el mismo día alguien se queda con una cuenta que no puede confirmar.
- 3Las cuatro plantillas de correo tienen que apuntar a la ruta de confirmación de la app; las que trae el proveedor por defecto van a su propio punto final, y entonces el enlace parece no hacer nada.
Por eso esta página enlaza el demo y no un formulario de alta: publicar un botón de crear cuenta que hoy devuelve un error sería peor que no publicarlo.