What it does, in plain terms
Picture a neighborhood watch tool with two sides to it. On one side, an administrator maintains a database of flagged individuals — a photo, a name, a risk level, a record of what happened. On the other side, anyone can point a webcam at a face and the system tells them, in real time, whether that face matches someone in the database, with a confidence score.
Separately, and just as central to the project, ordinary users can report incidents — theft, harassment, traffic violations — by dropping a pin on a map. Those reports aggregate into a live heatmap of where trouble is concentrated across a city's neighborhoods, which is useful on its own even without ever touching the face-recognition side.
The system also has a fairness mechanism built in: nobody gets permanently branded "criminal" from a single flag. Violations accumulate into escalating warning levels, and only repeated, ignored warnings lead to that classification. Anyone who believes they were flagged in error can file an appeal, which an admin reviews and can approve — instantly resetting their status and clearing the record.
Behind The Scene (BTS) / Under The Hood :
The face-matching itself runs in the browser via face-api.js, a TensorFlow.js library. Three pretrained models do the work in sequence: SSD MobileNet v1 finds a face in the video frame, a 68-point landmark model aligns it, and a recognition model reduces the aligned face to a 128-number "descriptor" — a compact numerical fingerprint. Matching a live face against the database is then just measuring Euclidean distance between descriptors; anything under a 0.6 threshold counts as a match, scored into a confidence percentage.
That matching math is cheap per-comparison but adds up across a large database, so it's offloaded to a small pool of Node.js worker threads on the backend rather than run on the main request thread — the server stays responsive to other requests (logins, dashboard queries, complaint submissions) while a match request works through the candidate list in the background.
The rest is a fairly conventional Express + MySQL API: JWT-authenticated routes, role-based access (admin vs. regular user), and a small rules engine that owns all the "what happens when" logic — warning-level thresholds, the criminal-classification rule, what a claim approval resets. Keeping that logic in one place instead of scattered across route handlers made it possible to reason about (and later verify) the actual business rules independently of the HTTP layer.
Why it's deployed the way it is
This is the part that made the deployment genuinely non-trivial, and worth explaining rather than glossing over. The backend can't run on Vercel's serverless functions, for two concrete reasons: the worker-thread pool needs to persist across requests (a serverless function's process doesn't survive between invocations), and uploaded images are written to local disk, which serverless functions don't reliably retain either. So the architecture is split three ways — the React frontend on Vercel, the Express API on Render (a normal, always-on Node process), and the database on TiDB Serverless, a free MySQL-compatible service chosen specifically so the existing SQL and mysql2 driver code didn't need rewriting for a different database engine.
Getting that split working end-to-end surfaced a batch of real, previously-invisible bugs — the kind that only show up once code actually runs somewhere real instead of just sitting in a repo:
- The test suite had never actually run. A single dependency (
@turf/turf, used for geofencing) shipped ES-module source that crashed Jest's parser onrequire(), which meant every test file failed to even load — silently, because nobody had looked at CI output that didn't exist yet. Underneath that crash, the tests that could theoretically run were mocking the database with the wrong shape entirely (db.execute()instead of the realdb.query()API, doubly-wrapped result arrays that didn't match what the actual driver returns). - Lint had never run either — there was no ESLint config file in the client at all, despite
npm run lintbeing a defined script. Adding one surfaced about 40 real issues: unused imports, a missingkeyprop, a couple ofclass=typos where React neededclassName=. - The most consequential bug: MySQL's prepared-statement protocol (which the
mysql2driver uses by default viapool.execute()) rejects bound parameters insideLIMIT/OFFSETclauses. Every paginated endpoint — the admin dashboard, notifications, the criminals list — was returning a 500 error in production with a cryptic"Incorrect arguments to LIMIT"message. The fix was a single line (switching topool.query(), which uses MySQL's older text protocol and doesn't have this restriction), but finding it meant actually deploying and clicking through the app rather than trusting that local development had proven it worked. - The seeded admin login (
admin@system.com/admin123, documented in the setup guide) never actually worked — the bcrypt hash committed alongside it wasn't really a hash of that password. Login just failed with a generic "invalid credentials" message and no server-side error to point at the real cause.
None of these were visible from reading the code in isolation. They only surfaced by actually standing the system up on real infrastructure and testing it like a user would — which is a fair description of most of the real engineering work on this project.
Tech stack
Frontend: React 18, Vite, Tailwind CSS, shadcn/ui (Radix primitives), face-api.js, Leaflet (maps), Recharts (dashboard charts) Backend: Node.js, Express, JWT auth, Multer, Worker Threads Database: MySQL (TiDB Serverless in production) CI/CD: GitHub Actions — lint, build, and test on every push and PR; automatic deploy to Vercel on merge Hosting: Vercel (frontend) · Render (API) · TiDB Serverless (database)