Skip to content
yalt8826.com
← projects/
projects/servgrid/in daily useandroid + websolo

ServGrid

The field-service app a UPS and battery business runs on: jobs, technicians, cash and customers, replacing paper.

Built solo · Sept–Nov 2026

01 / overview

Software that runs a real business every day.

A UPS and battery business ran on paper: job slips, a cash book, phone calls to find out where a technician was. ServGrid replaced it with one Android app for the field staff and a web console for the owner.

It has been in daily use for about a month. Every job is dispatched, tracked, completed and paid for through it, and every evening the cash collected in the field is handed over and reconciled in it.

8people use it every day
7+jobs a day, at minimum
4roles, each with its own app layout and permissions
647commits, built alone
02 / screens

What the staff see.

Names, phone numbers and the business's money figures are pixelated. Everything else is the real app.

01Owner dashboard
02Dispatch board and technician load
03Today's jobs
04A technician's job card
05Cash handover: expected vs declared
06The owner's web console
03 / who uses it

Four roles, four different apps in one.

Each role sees only what it needs. Permissions are defined once in a shared package, enforced on the server, and read by the app to decide what to render.

RoleUses it toCan't see
TechnicianSee the day's jobs, navigate, call the customer, complete the job with what was collected, hand over cashRevenue, other technicians' work
DispatcherTake calls, raise and assign jobs, watch technician load, chase overdue work, record maintenance contractsAny money from jobs, and where a technician is (only whether tracking is healthy)
Sales repLog sales to company accounts, record payments, send quotationsOther reps' accounts
OwnerEverything: performance, dues, cash confirmation, live location, staff, catalogue, on the phone or the web console—
04 / architecture

One small stack, on a PC in the office.

The owner chose to run the server on his own always-on office PC rather than rent one. It has no public address, so the phones reach it through a Cloudflare tunnel, and backups go off-site to Cloudflare R2.

clientsExpo · React Native
Android app (all roles)owner web consolebackground GPSOTA updates
edgeCloudflare
cloudflared tunnelPages (public site)
apiFastify · TypeScript
permission matrixidempotent writeszod schemasargon2 auth
dataDocker on the office PC
PostgreSQL 16MinIO (photos, payment proofs)R2 off-site backups
talks to
  • WhatsApp Cloud API: the customer gets a "visit scheduled" message and, after the job, an invoice with a UPI QR
  • Firebase push: wakes a phone when a job is assigned or the owner taps "locate now"
  • EAS: Android builds and over-the-air updates
05 / design decisions

Rules the database enforces, not rules people remember.

Most of the design work went into making mistakes impossible rather than unlikely. A business that hands cash between people can't afford "the app usually gets it right".

privacy by schemaDispatchers can't see revenue

Job money lives in a separate table that dispatcher queries never join, and a custom lint rule fails the build if dispatcher code ever touches it.

no stored totalsDues can't drift

What a company owes is a database view, sales minus payments. It updates the moment a sale is logged and can never fall out of sync after an edit or a void.

cashExpected cash is derived

A technician declares what he's handing over; what he should have comes from his cash jobs that day. Every variance in the owner's queue is a real discrepancy.

flaky networksEvery write is idempotent

Each submit carries a key minted once and reused on retry, so a timeout on a weak signal can't create a duplicate job or count a payment twice.

locationTracking that fails loudly

GPS runs only in work hours, buffers through basements, and shows a health chip ("last ping 6 min ago") so a silent failure gets noticed.

no lost workOnline-only, honestly

When the connection drops, a cover screen appears over whatever was open. Nothing typed underneath is lost, and nothing is silently queued.

06 / how it got built

Every big call was written down.

The plan was settled before the first line of code, then built in phases: 89 tasks, each naming the tests to write and the rollback if it didn't work. When the owner changed his mind, the change got a dated decision record.

  1. early Sept 2026Plan, then first commit

    Data model, permissions, screens for every role and a phase-by-phase build order, written before any code.

  2. 15 SeptDropped offline mode

    The first design gave field staff a full offline copy of their jobs. Syncing conflicts would have cost more than they saved, so every role went online-only, with no business data kept on a phone.

  3. 15 SeptMaintenance contracts

    Yearly maintenance contracts became real records, with the app reminding the dispatcher four months after a site's last visit.

  4. 19 SeptNo VPS: the server moves to the office PC

    The owner chose to host it himself. That meant a tunnel instead of a public address, and backups that had to leave the building.

  5. 23 SeptWhatsApp invoices with a UPI QR

    The official WhatsApp Cloud API sends from the business's own number, with the QR set by the owner from his profile.

  6. 24 SeptCloudflare at the edge, R2 for backups

    Also asked seriously and answered no: moving the API onto Cloudflare Workers.

  7. Oct 2026Live

    The staff switched from paper. Since then the work has been changes asked for by the people using it.

07 / keeping it correct

Tests that have to fail first.

Every pull request runs type checks, lint, unit tests, integration tests against a real Postgres in a container, and every database migration up, down and up again on a clean database. The three custom lint rules each come with a file that exists only to break them, so a rule that quietly stops working gets caught.

~150test files
89planned build tasks, each with exit criteria
10dated decision records
3custom lint rules, each proven by a failing fixture
08 / what was hard

Shipping was the easy part.

The hardest part wasn't writing the app. It was deploying it onto hardware I don't control, and then changing it once real people started using it. A spec reads differently once a technician is using it in a basement with one bar of signal.

What I'm proudest of is simple: I built something that people open every morning to do their job.