All work

/ Case study · Apr 2025 · rebuilt Oct 2026 · Weekend sprint, rebuilt solo

Police Record Management

A records workspace for Indian police stations. Officers upload a case file or photograph a handwritten FIR, fix inconsistent names with a mix of rules and Gemini, and keep an audit trail of every change inside the record itself. I built the first version in a 38-hour weekend sprint in April 2025, then rebuilt it on my own in October 2026 with handwritten-document OCR, verified accounts and server-side limits on every AI call.

Role
Sprint: lead developer across the app shell, AI flows, name validator, auth and UI. Rebuild: solo across design, frontend, server routes and Firestore rules
Team
Sprint: 2, with Ashish Gupta (crime-section API prototype and PR reviews). Rebuild: solo
Timeline
18–20 Apr 2025 sprint (20 commits in about 38 hours) · rebuilt Oct 2026 (18 commits)
Stack
Next.js, TypeScript, Gemini, Firebase
Police Record Management screenshot

/ 01

The problem

Case records exported from older police systems are messy. The same accused person shows up as 'RAJESH kumar', 'Rajesh Kumar' and 'Rajes Kumar' in different files. Inconsistent names make records hard to match and search, and cleaning them by hand is slow work that nobody tracks.

Most case files never reach a system at all. FIRs and case diaries are handwritten, often in Hindi, Bengali or another regional script, so the information in them stays on paper.

The sprint version solved the first problem but wasn't safe to leave online. The Gemini key shipped to the browser, one API route called Gemini for anyone who asked, guests could sign in without an account, and a crime-statistics page rendered HTML written by the model.

/ 02

Approach

I designed the app around the workflow a data-entry officer already has, which is a spreadsheet. You upload a CSV, see it as an editable table, run an audit, review the suggested fixes, and download a clean file. Nothing is changed without a person approving it.

For names I used two stages. Fast, predictable rules catch what rules are good at: digits, stray symbols, double spaces, bad capitalisation. Gemini then suggests spelling corrections, and has to explain each one. Every accepted fix is stamped into the row with who changed it, when and why.

The rebuild added a second way in. An officer photographs or scans a handwritten case document, and Gemini reads it into the same 14-column schema, one record per accused. It writes names in English while keeping the original-script spelling alongside, and flags any field it couldn't read with confidence. The officer checks each record beside the page before it joins the dataset.

I also took out what I couldn't defend. Guest sign-in and the crime-statistics page are gone. Every AI call now goes through the server, which checks the officer's sign-in and verified email and counts the call against a usage limit before Gemini sees anything.

/ 03

What I built

  • Handwritten document OCR

    Drop in JPG, PNG, HEIC or PDF scans of FIRs and case diaries, up to 4 MB each. Records come back field by field, with the accused's name in its original script kept for checking and low-confidence fields highlighted. You can edit, untick or remove a record before adding it.

  • Two-stage name audit

    A rule engine scores each name out of 100, then Gemini suggests corrections with a reason for each. You can apply them one at a time or all at once.

  • Audit trail in every row

    Applying a fix writes the date, the officer's email, a 'Corrected' status and a 'Corrected X to Y' remark into that row. Scanned rows are marked 'Imported via OCR', or 'Needs verification' if any field was uncertain.

  • CSV in, clean CSV out

    A quote-aware parser checks the file against the 14 expected columns and shows every cell as an editable field. Exports quote values properly and neutralise spreadsheet formula injection, so a cell can't run as a formula in Excel.

  • Ask AI about your data

    Ask questions in plain English, such as which accused are absconding by station, and Gemini answers using only the loaded file. The server accepts the question alone, and fixes the model settings and prompt size itself.

  • Verified, per-officer accounts

    Email or Google sign-in, a verified email before the console opens, 12-character passwords with a live checklist, and a password-reset flow. Firestore rules let each officer read and write only their own files.

/ 04

How it works

  1. Browser
    • Next.js 15.5
    • TypeScript
    • shadcn/ui
    • Device gate
  2. Identity & storage
    • Firebase Auth
    • Verified email
    • Firestore rules
  3. Server
    • ID-token check (jose)
    • Usage limiter
    • Genkit flows
    • OCR + query routes
  4. AI
    • Gemini 3.5 Flash
    • JSON-schema output
    • zod-typed flows
Parsing, editing and export happen in the browser. Every AI request carries the officer's Firebase ID token. The server verifies it against Google's public keys, requires a verified email, counts the call against per-user and global limits, and only then calls Gemini with a key the browser never sees.

/ 05

Hard parts

  1. 1

    Rules first, model second

    LLMs aren't consistent about simple formatting, and they cost a call per batch. So a deterministic validator runs first. It starts every name at 100 confidence and deducts points for each problem, and only the results go to Gemini.

    // validateBasicRules, simplified
    let confidence = 100;
    if (name.length < 3)                       confidence -= 30;
    if (/\d/.test(name))                       confidence -= 40;
    if (/[^a-zA-Z\u0900-\u097F\s.\-]/u.test(name)) confidence -= 30;
    if (/\s{2,}/.test(name))                   confidence -= 15;
    if (!isTitleCase(name))                    confidence -= 20;
    return { valid: confidence > 70, confidence };
  2. 2

    Locking down a public demo

    Server actions are public endpoints, so hiding a button proves nothing. Each audit action and API route now verifies the caller's Firebase token itself and refuses guest and unverified accounts. Anyone can sign up, so on top of each officer's hourly and daily limits there's a shared cap across all users. Counts live in server memory, so a Google Cloud quota stays as the hard backstop.

    // rate-limit.ts, simplified
    const LIMITS = {
      query: [{ limit: 30, per: 'hour' }, { limit: 200, per: 'day' }],
      ocr:   [{ limit: 40, per: 'hour' }, { limit: 200, per: 'day' }],
      audit: [{ limit: 10, per: 'hour' }, { limit: 50,  per: 'day' }],
    };
    // Sign-up is open, so every Gemini call also counts here
    const GLOBAL = [{ limit: 600, per: 'hour' }, { limit: 3000, per: 'day' }];
  3. 3

    Reading handwriting without inventing it

    A model asked to fill a form will happily fill it. The OCR prompt says to leave unreadable fields empty and never guess, and a response schema makes Gemini list every field it was unsure of, using only the real field names. The server then cleans up the reply before the browser sees it. Uploads stop at 4 MB because Vercel rejects request bodies over 4.5 MB. A busy model (503) is retried with backoff, but a used-up quota (429) fails straight away, because waiting won't fix it.

  4. 4

    Turning AI output into table edits

    Gemini returns a list of names, not cell positions, so each suggestion has to be traced back to its row and column before it can be applied. The rule-based pass now keeps row numbers, but Gemini's suggestions are still matched back by value across the three name columns, so identical names in different rows collapse to the first match. Asking the model to echo back a row and column for each suggestion is the proper fix.

  5. 5

    Bias is a correctness bug

    To make the model respect regional naming conventions, I gave it examples of 'wrong' names. Looking back, many of those examples are real names that combine different communities or regions. In a police system that's a bias risk, not a style choice. The list is still in the prompt. The fix is to limit the model to formatting and spelling, so it never 'corrects' a name just because the combination looks unusual.

/ 06

Design

The rebuild swaps the warm near-black and pale teal for a cooler, more official look: a near-black slate, one muted steel-blue accent, green for applied changes and amber for anything still waiting, such as the 'Corrections pending' count. Geist Sans and Geist Mono keep dense tables readable, corners are tight, and the national emblem with a plain wordmark sets the tone.

A new landing page sets out the problem, the six capabilities, a four-step workflow and the governance rules before it asks anyone to sign in. Its hero shows a sample of the console, including a scan turning रमेश कुमार यादव into Ramesh Kumar Yadav. The console is a dense dashboard with a row of key numbers, a case-file panel, and an editable table with sticky headers and row numbers.

The console is desktop-only on purpose, because wide case tables don't work on a phone. Middleware sends phones and tablets to a block page, and a browser-side check catches iPads that report a Mac user agent. The password-reset page stays open on mobile, because that's where people open the email.

  • Slate#0A0C10
  • Surface#101318
  • Text#DBE0E6
  • Steel blue#3E80CC
  • Applied#389F6F
  • Pending#E79F23

/ 07

Outcome

  • The sprint shipped a working end-to-end flow in about 38 hours: upload, edit, audit, AI corrections, audit trail and export, plus a per-user file library and AI Q&A.
  • The rebuild is live on Vercel at a new address. Anyone can create an account, but the console only opens after email verification, and every AI feature is metered per user and across all users.
  • Around 7,100 lines of app code, not counting the generated UI components, with three Genkit flows and two authenticated API routes. Guest sign-in and the crime-statistics page were removed rather than patched.

/ 08

What I learned

  • Treat every server action as a public endpoint. AI features became safe to leave online only when each one checked the caller's token itself, rather than trusting the UI to hide them.
  • Cutting code can be the security fix. Removing guest access and the page that rendered model-written HTML closed more holes than patching them would have.
  • Measure before you claim. The next step is a small labelled set of messy names and handwritten scans with an eval script, so correction and OCR accuracy become numbers rather than impressions.
  • In public-sector tools, a human approves every change. Suggest-and-approve with an audit trail now covers OCR imports too, which keeps AI an assistant rather than an authority.

/ 09

Screens

Landing page headline 'Accurate case records, from handwritten FIR to audited database' beside a console sample with a name-corrections table and an OCR card turning a Hindi name into Ramesh Kumar Yadav, above three problem statements
The new landing page: the problem and a sample of the console, before any sign-in.
Capabilities grid with six cards: handwritten document OCR, name auditing, reviewed corrections, plain-language queries, secure case repository and audit trail
Six capabilities, from scanning a handwritten FIR to the audit trail.
Governance section titled 'Built for records that carry legal weight' with cards for authenticated access, officer approval of every change, traceable changes and standard formats
Governance: the rules that keep AI an assistant rather than an authority.
Split-screen sign-in with the national emblem and three feature highlights on the left and a Welcome Back form with email, password and Google sign-in on the right
Sign-in: email or Google only, with guest access removed.

/ Next case study

TokenFund

Tokenized crowdfunding on Stellar. Founders launch campaigns with a token, a goal and milestones, and a set of Soroban contracts handles ownership tokens, investments, refunds and governance.