Keepwell
Maintenance operations for estates, factories, campuses and hotels; the technician app works with no signal.
- 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
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
Someone reports it
A resident scans the QR sign in the lobby or plant room and says what is wrong. No app, no account.
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.
The technician does it
Today's jobs, procedures with measured limits and permits with locks, all on the phone, saved there first.
It syncs and closes
Changes send when the signal returns, once each. The manager sees it resolved, with the audit trail.
Engineering challenges
-
Two technicians, one last part
With 200 reservers for 37 units, exactly 37 succeed, 163 are refused, and the ledger reconciliation reports no drift.
-
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.
-
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
| Decision | What I chose | Instead of |
|---|---|---|
| A drawing room, not a dashboard | Datum: sites drawn as sections, one wayfinding yellow, its own typefaces | A component-library dashboard |
| A score that shows its working | A 0–100 dispatch score split into four visible parts | |
| Audit, then fix with tests | An 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
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.
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
yousofselim2@gmail.com
