Keepwell

Maintenance operations for estates, factories, campuses and hotels; the technician app works with no signal.

Live demo (opens in a new tab)

Role
Sole developer: product spec, architecture, database and security design, design system, and review of the code Claude Code agents wrote under my direction
Dates
Nov 2026 – present
Status
Live demo · pre-pilot
Stack
Java, Spring Boot, PostgreSQL, Angular · race-tested stock

Keepwell, screen by screen

  • Overview. The manager's day starts with what needs a decision.

  • Dispatch. Every suggestion shows the parts of its score.

  • Technician, phone. The technician's next job, saved on the phone.

  • Permit. A permit with its lock, its proof and every approval.

  • Landing. Sites drawn as sections, the way an engineer would.

In short

  • Stock that cannot be double-booked

    A part is reserved by one conditional UPDATE. In the concurrency test, 200 people race for 37 units: exactly 37 succeed and the ledger reconciles with no drift.

  • Works in a plant room

    Each technician action is saved on the phone with its own ID, so a replay is applied once, and a clash is shown to the person, never silently overwritten.

  • Isolation enforced in the database

    Row-level security is forced on all 71 tenant tables and also limits site-scoped staff, so a forgotten filter in the code still returns nothing it should not.

The problem

Maintenance is run across buildings and shifts by people who are rarely at a desk, and the tools they are given assume a desk and a signal.

  • The technician doing the work is often in a basement, lift shaft or plant room where the phone has no signal.
  • Two technicians can be promised the last spare part, and the second finds out at the store.
  • One installation serves many organisations and many sites, and one missed filter leaks another customer's jobs.
  • Dangerous work needs a permit, a lock on the isolation point and a signed procedure, which usually live on paper.

How it works

  1. Someone reports it

    A resident scans the QR sign in the lobby or plant room and says what is wrong. No app, no account.

  2. A dispatcher assigns it

    Each suggested technician shows a score out of 100, split into skills, availability, workload and distance. Expired certifications are never relaxed.

  3. The technician does it

    Today's jobs, procedures with measured limits and permits with locks, all on the phone, saved there first.

  4. It syncs and closes

    Changes send when the signal returns, once each. The manager sees it resolved, with the audit trail.

From a report in the lobby to a closed job, with or without signal.

Engineering challenges

  1. Two technicians, one last part

    With 200 reservers for 37 units, exactly 37 succeed, 163 are refused, and the ledger reconciliation reports no drift.

  2. Offline without lost or doubled work

    A replay returns the stored result instead of applying twice, and a clash reaches the technician as a conflict to resolve, never a silent overwrite.

  3. One database, many customers and sites

    A scoped manager's raw SQL sees only Line A, updates outside it affect nothing, and the scoped list still uses an index range: 0.9 ms with 20,000 other rows.

Technical decisions

DecisionWhat I choseInstead of
A drawing room, not a dashboardDatum: sites drawn as sections, one wayfinding yellow, its own typefacesA component-library dashboard
A score that shows its workingA 0–100 dispatch score split into four visible parts
Audit, then fix with testsAn OWASP ASVS 5 Level 2 audit, then fixes with regression tests

Results

backend tests passing
273
racing reservations succeed, for 37 units in stock
37 of 200
tenant tables with forced row-level security
71
critical findings in the ASVS audit; all 5 high fixed
0

From the milestone reports and the security audit of 12–13 November 2026, on synthetic demo data. No customers, uptime or impact figures yet.

Reflection

What worked

  • Writing each hard rule down as a decision record first, then a test that must fail before the fix, kept agent-written code honest.
  • Putting isolation in the database meant the security review could check one mechanism instead of every query.

What I underestimated

  • How much of the risk sits around the core: upload decoding, proxy headers, first-run setup and CI supply chain, not the tenancy design.
  • How fast agents produce plausible code, and how much reading it takes to be sure it is right.

What I would do next

  • Close the remaining audit items and get an independent penetration test.
  • A randomised-disconnect harness for sync, and a load test of the reservation path.
  • A pilot with a real maintenance team before any claim about use.

Full engineering notes

8 sections, about 6 minutes to read

Summary

Maintenance teams in property estates, factories, campuses and hotels still run on WhatsApp, spreadsheets and paper permits, and the technician doing the work is often in a basement with no signal. Keepwell is an offline-first maintenance operations platform for them. Managers see what is late across every building; dispatchers assign jobs with a technician score that shows its working; technicians follow step-by-step procedures with measured limits, work under permits with lockout and tagout, and reserve spare parts that two people can never both take. Planned maintenance, purchase orders and analytics live in the same record. The technician app is an installable PWA: every action is saved on the phone and syncs once, in order, when the signal returns. I designed and built it alone on 12 and 13 November 2026, with Claude Code agents writing much of the code under my direction and review; it is pre-pilot and the live demo runs on synthetic data.

My role

What I owned

  • The product specification, the module boundaries and the 29 architecture decision records.
  • The database design: forced row-level security for tenants and sites, the stock ledger and the reservation rule.
  • The offline sync protocol, its conflict rules and the technician PWA's durable queue.
  • The Datum design system, the Riser logo and the Keepwell Sans and Mono typefaces.
  • Direction and review of the code: Claude Code agents wrote much of it; I set each task, read the result and sent it back when it was wrong.

What I did not own

  • Claude Code agents did much of the typing; the decisions and review are mine.
  • Spring Boot, Angular, PostgreSQL, the Go standard library and the other open-source libraries it is built on.
  • Keepwell Sans and Mono are modified from the open-licence Familjen Grotesk and Martian Mono.
  • No customer, user research, outside code review or penetration test yet.

Constraints

  • One developer, many modules

    Work orders, dispatch, planned maintenance, permits, procedures, stores, purchasing and analytics share one owner. That ruled out microservices: one Spring Boot process over one database, with module boundaries enforced by a test, and one extra service, the Go notifier.

  • A pilot budget

    Two NAT gateways would cost more than the whole pilot, so the AWS design has none: tasks reach AWS services directly, with ingress closed and egress limited to the ports they need.

How each challenge was solved

Two technicians, one last part

Why it was hard

A storekeeper's worst day is promising the same spare part twice. Reading the stock level and then writing it back leaves a gap where two requests both see one left.

What I built

A reservation is a single UPDATE that adds to the reserved count only where on hand minus reserved still covers the quantity. A concurrent request waits on the row lock, PostgreSQL re-checks the condition on the newest row, and the loser gets a 409 with other stores that have the part. Multi-item reservations lock rows in a fixed order; a CHECK constraint is the last line.

Options considered
  • Lock the row with SELECT … FOR UPDATE, check in Java, then update
  • Optimistic locking with a version column and retries
  • SERIALIZABLE isolation with retries
  • One conditional UPDATE under READ COMMITTED
Trade-off

Every stock change must go through one ledger service, a rule enforced in review and documented for every contributor.

Validation

ReservationConcurrencyIT: 2, 50 and 200 reservers released together by a barrier; LedgerInvariantPropertiesIT for the ledger sums.

Offline without lost or doubled work

Why it was hard

A technician can finish three jobs underground, lose the phone's connection halfway through a send, and retry. Sending whole records back would let the last writer silently erase someone else's change.

What I built

Each action is stored in IndexedDB as a mutation with a client ID and the version it was based on. The server deduplicates on tenant, person and that ID, and answers each one applied, duplicate, conflict or rejected. Notes and photos append; a checklist item merges unless it changed since; a status change is re-checked against the state machine.

Options considered
  • Send whole records, last write wins
  • CRDTs for every entity
  • Read-only offline
  • A log of small mutations, each with its own ID
Trade-off

Only eight kinds of action can be done offline, and the server must keep every result so a replay gets the same answer.

Validation

Sync replay tests in SyncIT; the queue's ordering, dedupe and backoff in the web unit tests.

One database, many customers and sites

Why it was hard

Large customers want a technician on Line A to see Line A only. Filtering in every query works until one query forgets, and that query leaks another site or another company.

What I built

The app connects as a role that cannot bypass row-level security, which is forced on every tenant table. Each transaction sets the tenant and the person's site paths; restrictive policies match rows by a location path that triggers keep current. Cross-tenant requests return 404, not 403.

Options considered
  • Filters in the service layer only
  • Row-level security with a join to sites per row
  • Row-level security on a stored location path
Trade-off

Every tool that runs SQL as the app role must set the scope first; a store with no site is invisible to scoped staff, which fails closed.

Validation

Tenant isolation, site-scope and authorisation-matrix integration tests; raw SQL as the app role in SiteScopeIT.

Why I made each decision

A drawing room, not a dashboard

Because

The first interface used the standard kit of stat tiles, pills and a stock font, and I rejected it as generic. Datum borrows from engineering drawings: buildings as stacks of levels, a revision cloud where something is past its time, and status as words, not coloured dots.

Trade-off

Every component is drawn by hand on the Angular CDK, and AA contrast for every colour pair is a unit test.

A score that shows its working

Because

A dispatcher will not trust a ranking they cannot argue with. Each suggestion shows skills, availability, workload and distance, and hard rules such as an expired certification mark a technician not eligible instead of lowering the score.

Audit, then fix with tests

Because

Multi-tenant software with public QR forms and webhooks has a large surface. The audit found no critical issue and five high ones, including an image upload that could exhaust memory and an open first-run setup; all five were fixed, and an independent re-review's findings each got a test that failed before the fix.

Trade-off

Some medium items remain partly open, such as trusted client IPs; they are listed in the audit, not hidden.

Testing

The backend runs unit and integration tests against a real PostgreSQL in Testcontainers: concurrency races, property-based ledger and meter tests, an authorisation matrix of every route against every role, isolation tests for every tenant table and an OpenAPI drift check. The web app has Vitest unit tests and Playwright end-to-end tests with axe-core.

backend tests passing (73 unit, 200 integration)
273
tenant tables with forced row-level security
71
high-severity audit findings, all fixed
5

Results in detail

  • A live demo anyone can open

    A role chooser opens the manager's overview, the dispatch board or the technician's phone app on synthetic estates, a factory included, with no sign-up.

  • Ready to self-host

    One Docker Compose file starts the database, the API and the web app; a first-run wizard sets up the organisation, buildings, assets and QR labels.

Stack and release record

  • Platforms

    Web: the desk app for managers, dispatchers and storekeepers, Technician PWA: installable, works offline, Self-host with one Docker Compose file

  • Built with

    Java 25, Spring Boot 4.1 (modular monolith), PostgreSQL 17 with row-level security, Angular 22, IndexedDB (offline queue), Go (notifier: SQS, signed webhooks), Terraform (AWS: ECS Fargate, RDS, CloudFront, WAF), OIDC single sign-on, SCIM, API keys, TOTP, Testcontainers, Vitest, Playwright, axe-core, Google Cloud Run (demo)

  • Testing and delivery

    273 backend tests passed on 13 November 2026, against a real PostgreSQL in Testcontainers, with Vitest and Playwright with axe-core for the web app. The demo runs on Google Cloud Run; the AWS Terraform is checked statically and not applied.

  • Status in full

    Pre-pilot. The live demo is a preview on Google Cloud Run with synthetic data and no real customers; the first load after a quiet spell can take about 20 seconds. The AWS infrastructure is written in Terraform and checked statically, but has not been applied. The source is private; a public showcase repository explains the build.

Next case study Adelante Daily-quote widgets for iPhone and Apple Watch, with one rotation rule in Dart, Swift and Kotlin.

All work and archive

Hiring interns for January 2027?

I’m available full-time from 25 January to 23 April 2027, and for graduate roles in Malaysia or overseas from August 2027. Email is the quickest way to reach me.

  • Subang Jaya, Malaysia
  • local time