council · parking-pwa

private enforcement · field tooling

a private parking pwa, no install required on cheap android.

a council contractor needed an internal field-team app that worked on the cheap android phones their wardens actually carried, behind their identity provider, with no app-store overhead.

the problem

the field team carries cheap android phones. they get dropped, lost, swapped, replaced from a drawer. there is no mdm, no enrolment ritual, no patient onboarding flow. a warden who shows up for a shift needs to be reporting within a minute of unlocking the device they were handed.

a native app fights every part of that. the play store wants signing, review, version pinning, and a story for forced upgrades. on a fleet that turns over informally, every install is a new support ticket. push the wrong build to the wrong device and the warden silently logs evidence into a stale schema for a week. a native shell on top of a webview would have inherited the worst of both worlds.

a generic web app gets closer but loses on two fronts. identity has to live somewhere. standing up an oidc provider, a user table, password resets, and a session story for a six-person field team is more code than the actual reporting flow. and the moment you ask “where do the photos go”, you are choosing between a storage account someone has to own and a dropbox-style integration that puts the contractor’s evidence chain inside a third-party folder hierarchy. neither is what the contractor wanted to inherit.

the budget shaped the rest. this is a small private-enforcement contract, not a council ito tender. there is no room for a bespoke platform, but the work still needs to stand up under the disputes tribunal: photos that survive a challenge, plate text the warden actually typed, gps captured at the moment of the shot, an audit trail tied to the person who filed it. cloudflare’s primitives sized to that exactly. workers for the api, pages for the pwa shell, r2 for the photo bytes, d1 for the canonical record, and access for the identity boundary. one vendor, four bindings, no servers to keep alive between shifts.

why we shaped it this way

we looked at react native, capacitor, and ionic before settling on a plain pwa. all three solve a problem we did not have: shipping the same codebase to the app stores. the field team does not want to be in the play store. the contractor does not want to maintain a developer account. once you remove “must ship to a store” from the requirement set, the wrappers stop earning their weight.

a pwa, served from a single host behind cf access, gets us the things that actually matter. zero install friction: the warden opens the url, signs in with a one-time email code, adds it to home screen, and the next shift it opens like an app. sso without a separate identity stack: cf access handles the otp flow, issues a jwt, and the worker verifies it via jwks. adding or removing a warden is one line in an access policy, effective in minutes. r2 holds the photo bytes at rates where six months of evidence costs less than a developer’s lunch, and d1 holds the structured record next to it.

the honest tradeoff: ios push and background sync are still partial. we chose online-first with retry-on-foreground rather than pretend the service worker can ride out a real dead-zone. site coverage is good enough that this hasn’t bitten, but it’s the first thing we’d revisit if a warden started filing reports from somewhere with no signal.

outcome

three weeks from kickoff to a working install on the field team’s devices. the pwa runs from one host, the worker on the same origin, photos in r2, records in d1, identity gated by cf access. adding a warden is a policy edit. removing one is a policy edit. evidence sits in storage the contractor controls, behind their auth, with the swap points written down so a future move to a different sql database or s3-compatible bucket is a few hundred lines, not a re-platform. the shape of the engagement is a small upfront build with a monthly retainer for ongoing changes, which matches the way the contract actually breathes: a few hours of tuning a month, more when a new site or report type comes online.

stack cloudflare workers · cloudflare pages · r2 · d1 · cf access
build 3 weeks
operate in flight, monthly
install steps 0 (pwa, no store)
auth boundary cf access · sso
device fleet mixed android, no mdm