~/henry-cabello
CVEspañol
← back to ./work
In development

Femi

One tap raises an alert, the people nearby who opted in get told, and the emergency contacts get an SMS. Between emergencies it is a local forum, because an app you only open in the worst moment of your life is an app you will not have installed. It is not shipped: no users, not deployed. What follows is a rewrite that cut the codebase to a ninth, and an audit that found the gap a green dashboard could not.

Role
Solo: product, backend, front-end, native shell
Stage
Substantially built, never released
Runs on
PostgreSQL, Capacitor for Android, Firebase for push
Repository
Private
The Femi dashboard. A large SOS button ringed by the helpers who would answer it, above rows for nearby alerts, the safety circle, identity and community.The safety map, showing active alerts graded by severity inside the help radius.The profile screen, with identity verification, responder toggles, a 25 km help radius, and a light, dark and auto appearance switcher.
Dashboard, safety map and profile, captured from the running application against Postgres with the seeded data.

$ cloc --diff v1 v2

Three codebases pretending to be one product

v1 · React Native + Express77,044
46,118 mobile/src · 30,926 backend/src · 70 screens
v2 · one Next.js app9,030
17 pages · 19 API routes · 55 components · 8 services

Hand-written application code, excluding dependencies and build output. Same product surface, roughly one ninth of the code.

v1 was a React Native app, an Express backend, and the hand-rolled contract between them. The tree told its own story. A legacy route folder inside the live app, and a matching legacy folder inside the alert screens. A file named verification_old.js at 1,903 lines. A screen called AlertDemo11_NightGuardian, demo eleven, at 1,996 lines. Three loose HTML files at the repository root for debugging face verification by hand. Nobody plans this. It accretes, one working session at a time.

The reduction is not that Next.js beats Express. It is that v1 paid for everything three times: a React Native screen, an Express route, and a serialization contract that both had to honour and neither enforced. v2 collapses the boundary. Server components call service modules directly, and Capacitor wraps the same web build for Android. The mobile app is the same build, so there is nothing to drift.

$ cat decisions.md

Capacitor instead of React Native

The product is mostly forms, lists and text. The genuinely native needs are two: background geolocation and push. Capacitor buys both with plugins and costs one web build instead of two codebases. A WebView will never match React Native on animation-heavy screens, and I am betting the product never needs one. If the map ever demands sixty-frame gesture work, this gets revisited.

Raw SQL instead of an ORM

PostgreSQL through the pg driver, hand-written SQL, numbered migration files. The nearby-alerts query is a haversine calculation and it reads more clearly as SQL than as any ORM expression I could write. The cost is no type safety between the database and the application, and that cost came due during this audit, in the coordinate bug below. I would make the same call at this size and not at three times it.

Face matching in the browser

v1 sent the selfie to AWS Rekognition. v2 compares it on the device, so the photo never leaves it and every match costs nothing. It also put the trust boundary on the wrong side, and I did not see that until I audited the endpoint. The browser computed the score and posted it, and the server wrote verified whenever it cleared the threshold. Anyone could post a 99 to that route and become a verified member. Verified members are who alerts get routed to, so that was the load-bearing claim in the whole product. The fix does not pretend to solve it. Until the comparison runs server side, a passing client score parks the record at pending for review, and a failing one is still a rejection. The same audit found a second numeric column the interface declared as a number, which is the raw SQL trade-off above coming due twice.

Next.js 16React 19TypeScriptCapacitor 8PostgreSQLpgBetter AuthLeafletFirebase FCMUploadThingTailwind 4Vitest

$ ./bin/audit --against-its-own-dashboard

A process that counts parts cannot see connections

v2 was built as a queue of specified tasks. Each one carried its own acceptance criteria and closed when they passed, which is how eight features and fifty four tasks reached done. Before writing this up I audited the result against that dashboard. Every green check on the left was true, and every question on the right was one the loop had no step for.

What the process measured
Tasks completed54 / 54
Features shipped8 / 8
Tests passing91 / 91
Typecheckclean
What nothing measured
Was any of it mounted?
Did the production build run?
Was the home screen real?
Did the tab bar lead anywhere?
Four core components were mounted nowhere

The app-group layout was a single div. The tab bar, the header, the location provider and the notification provider were all built, styled and unit tested. A grep for all four found zero references outside their own files. Every one had passing tests, because every test mounted it directly.

The alert detail page crashed on every render

Calling toFixed on a latitude threw, and the page fell to the error boundary. The pg driver returns numeric columns as strings, but the Alert interface declared them as numbers, so TypeScript could not see it, and the service tests mocked rows with no coordinates at all. This is the screen a responder opens from a push notification.

Home was a placeholder and two tabs led nowhere

The home screen read "coming soon". The tab bar linked to a map route and a profile route; neither existed, and there was no mapping library in the dependency list at all. The production build also failed, on a package whose export map resolves to its Node build first. The dev server resolved it. The test runner resolved it. Nothing in the process ever ran the production build.

Unit tests verify that a part works. Nothing asserted the parts were connected. The correct module, never wired in, is invisible to every test that imports it and to every type checker, and it survives any process that measures progress by counting finished parts.

$ git log --oneline develop

The repair, and how I know it holds

Pinned the browser build of the face-matching package. Wrote the app shell. Rebuilt the location provider from a pass-through into a real context, which fixed a live bug on the way: its callback setter replaced rather than appended, so two subscribers would fight and the last to mount would silently win. Coerced the coordinate columns at the service boundary, so the interface matches what the driver returns.

Built the dashboard, the profile page and the map. Fixed an SOS bug where a missing GPS fix sent the alert to zero, zero, which is a point in the Gulf of Guinea and invisible to every proximity query; it now takes a one-shot fix and refuses rather than send a lie. Added a seed script and a migration runner, because a fresh clone could not previously reach a working database.

Then tests for the composition root and the coordinate bug, verified by mutation: I reverted each fix, confirmed the test went red, and restored it. One of the five shell tests passes either way by design, because it asserts only that children render, which the bare div also did. That one is the control.

117 / 117Tests passing
39Routes building
0eslint warnings
4Of 5 tests, red on revert

$ cat STATUS

Where it stands

Sign-in, dashboard, map and a settings write all confirmed against Postgres. It is not deployed and it has no users, and the next thing it needs is not more code. It is one real neighbourhood willing to install it, because a safety network with nobody in it protects nobody.