Femi
Un toque levanta una alerta, las personas cercanas que aceptaron reciben aviso, y los contactos de emergencia reciben un SMS. Entre emergencias es un foro local, porque una app que solo abres en el peor momento de tu vida es una app que no vas a tener instalada. No está lanzada: sin usuarios, sin desplegar. Lo que sigue es una reescritura que dejó el código en un noveno, y una auditoría que encontró lo que un tablero en verde no podía.
- Rol
- Solo: producto, backend, front-end, capa nativa
- Etapa
- En gran parte construida, nunca lanzada
- Corre en
- PostgreSQL, Capacitor para Android, Firebase para push
- Repositorio
- Privado



$ cloc --diff v1 v2
Tres bases de código que fingían ser un producto
Código de aplicación escrito a mano, sin dependencias ni salida de build. La misma superficie de producto, aproximadamente un noveno del código.
v1 era una app en React Native, un backend en Express, y el contrato hecho a mano entre los dos. El árbol contaba su propia historia. Una carpeta de rutas legacy dentro de la app viva, y otra carpeta legacy igual dentro de las pantallas de alertas. Un archivo llamado verification_old.js con 1.903 líneas. Una pantalla llamada AlertDemo11_NightGuardian, demo once, con 1.996 líneas. Tres archivos HTML sueltos en la raíz del repositorio para depurar verificación facial a mano. Nadie planea esto. Se acumula, una sesión de trabajo a la vez.
La reducción no es que Next.js le gane a Express. Es que v1 pagaba todo tres veces: una pantalla de React Native, una ruta de Express, y un contrato de serialización que ambos debían respetar y ninguno hacía cumplir. v2 colapsa la frontera. Los server components llaman a los módulos de servicio directo, y Capacitor envuelve el mismo build web para Android. La app móvil es el mismo build, así que no hay nada que se desincronice.
$ cat decisions.md
El producto es sobre todo formularios, listas y texto. Las necesidades realmente nativas son dos: geolocalización en segundo plano y push. Capacitor compra ambas con plugins y cuesta un build web en vez de dos bases de código. Un WebView nunca va a igualar a React Native en pantallas con mucha animación, y estoy apostando a que el producto nunca necesita una. Si el mapa alguna vez exige gestos a sesenta cuadros, esto se revisa.
PostgreSQL con el driver pg, SQL escrito a mano, migraciones numeradas. La consulta de alertas cercanas es un cálculo de haversine y se lee más claro como SQL que como cualquier expresión de ORM que yo pudiera escribir. El costo es que no hay seguridad de tipos entre la base de datos y la aplicación, y ese costo se cobró durante esta auditoría, en el bug de coordenadas de abajo. Tomaría la misma decisión a este tamaño y no al triple.
v1 mandaba la selfie a AWS Rekognition. v2 la compara en el dispositivo, así que la foto nunca sale de ahí y cada comparación no cuesta nada. También dejó la frontera de confianza del lado equivocado, y no lo vi hasta que audité el endpoint. El navegador calculaba el puntaje y lo enviaba, y el servidor escribía verificado apenas superaba el umbral. Cualquiera podía enviar un 99 a esa ruta y volverse miembro verificado. Los miembros verificados son a quienes se enrutan las alertas, así que esa era la afirmación que sostenía todo el producto. El arreglo no finge resolverlo. Hasta que la comparación corra en el servidor, un puntaje que pasa deja el registro en pendiente para revisión, y uno que falla sigue siendo un rechazo. La misma auditoría encontró una segunda columna numeric que la interfaz declaraba como número, que es la contrapartida del SQL crudo de arriba cobrándose dos veces.
$ ./bin/audit --against-its-own-dashboard
Un proceso que cuenta partes no puede ver conexiones
v2 se construyó como una cola de tareas especificadas. Cada una llevaba sus propios criterios de aceptación y se cerraba cuando pasaban, y así ocho funciones y cincuenta y cuatro tareas llegaron a terminado. Antes de escribir esto audité el resultado contra ese tablero. Cada visto verde de la izquierda era cierto, y cada pregunta de la derecha no tenía paso en el ciclo.
El layout del grupo de la app era un solo div. La barra de pestañas, el encabezado, el proveedor de ubicación y el de notificaciones estaban construidos, con estilos y con tests unitarios. Un grep de los cuatro encontró cero referencias fuera de sus propios archivos. Todos pasaban sus tests, porque cada test los montaba directamente.
Llamar toFixed sobre una latitud lanzaba error, y la página caía al error boundary. El driver pg devuelve las columnas numeric como texto, pero la interfaz Alert las declaraba como números, así que TypeScript no podía verlo, y los tests del servicio simulaban filas sin coordenadas. Esta es la pantalla que abre quien responde desde una notificación push.
La pantalla de inicio decía "muy pronto". La barra de pestañas apuntaba a una ruta de mapa y una de perfil; ninguna existía, y no había librería de mapas en las dependencias. El build de producción también fallaba, por un paquete cuyo mapa de exports resuelve primero a su build de Node. El servidor de desarrollo lo resolvía. El runner de tests lo resolvía. Nada en el proceso corría nunca el build de producción.
Los tests unitarios verifican que una parte funciona. Nada afirmaba que las partes estuvieran conectadas. El módulo correcto, nunca conectado, es invisible para todo test que lo importa y para todo verificador de tipos, y sobrevive a cualquier proceso que mida el avance contando partes terminadas.
$ git log --oneline develop
La reparación, y cómo sé que se sostiene
Fijé el build de navegador del paquete de comparación facial. Escribí el shell de la app. Reconstruí el proveedor de ubicación, de un pass-through a un contexto real, lo que arregló un bug vivo de paso: su setter de callbacks reemplazaba en vez de agregar, así que dos suscriptores peleaban y ganaba en silencio el último en montarse. Convertí las columnas de coordenadas en la frontera del servicio, para que la interfaz deje de mentir.
Construí el tablero, la página de perfil y el mapa. Arreglé un bug de SOS donde la falta de señal GPS mandaba la alerta a cero, cero, que es un punto en el Golfo de Guinea e invisible para toda consulta de proximidad; ahora toma una lectura única y se niega en vez de mandar una mentira. Agregué un script de datos de prueba y un corredor de migraciones, porque antes un clon nuevo no podía llegar a una base de datos funcionando.
Después, tests para la raíz de composición y para el bug de coordenadas, verificados por mutación: revertí cada arreglo, confirmé que el test se ponía en rojo, y lo restauré. Uno de los cinco tests del shell pasa igual en ambos casos por diseño, porque solo afirma que los hijos se renderizan, cosa que el div pelado también hacía. Ese es el control.
$ cat STATUS
En qué quedó
Inicio de sesión, tablero, mapa y una escritura de ajustes, todos confirmados contra Postgres. No está desplegada y no tiene usuarios, y lo próximo que necesita no es más código. Es un barrio real dispuesto a instalarla, porque una red de seguridad sin nadie adentro no protege a nadie.