Skip to main content
Back to Blog
October 6, 2026 3 min read

No Framework, No Build Step, No package.json

ArchitectureSupabasePostgres
No Framework, No Build Step, No package.json

A mid-sized Ontario municipality runs dozens of road resurfacing projects each construction season. Before this tool, the status of each lived in spreadsheets and email threads. Residents phoned to ask why their road was torn up; staff answered the same question repeatedly; there was no single source of truth.

The brief had two halves. Put the whole season on one public map anyone can open without a login — and let the project managers keep it current without a developer in the loop.

What shipped has no framework, no build step and no package.json. Three static files — a public map, a self-contained admin page, and a stylesheet — talking directly to Supabase's PostgREST API.

Why that was the right answer

This is a tool a municipality will own for a decade. The question that should govern the stack is not "what would be pleasant to write" but "what will still build in 2034".

A build step is a dependency on a toolchain, which is a dependency on a Node version, which is a dependency on somebody keeping it current. Three files served as-is have none of that. The person who maintains this in five years opens a file and reads JavaScript.

Authorization is Row Level Security, not application code: anonymous reads for the public map, authenticated writes for the admin. Sign-ups are disabled in Supabase Auth, so admin accounts exist only by invitation — adding or revoking an editor is a click in a dashboard rather than a code change and a deploy. That is the difference between a tool the client operates and a tool the client has to call someone about.

Deployment is a Bitbucket-to-Amplify pipeline whose build step substitutes credentials into placeholder tokens in the source, so the repository carries no keys.

Modelling progress honestly

The interesting modelling problem was not the map. It was progress.

A road is not "60% done". It moves through a sequence of work activities, and surface-treatment roads follow a completely different sequence — pulverizing, then gravel/grade/compact, then surface treatment — than standard overlay roads. A single percentage column would have been easy, wrong, and impossible to correct later without a migration and a conversation about what the old numbers meant.

So activities are a child table keyed by activity type, and the detail panel renders one progress card per activity with its own percentage and scheduled dates. A third level tracks individual driveways — around 470 of them, bulk-imported from the GIS department's export, each one's parent road resolved from its UID prefix.

Degrading gracefully

If Supabase is unreachable, the map falls back to a bundled GeoJSON geometry file and a CSV status snapshot. The public map still renders; the data is just stale.

For a public-facing municipal tool this is not a nicety. The people opening it are residents who want to know why their street is closed, and "we are having a database issue" is a worse answer than slightly old information clearly rendered.

Where the boring stack bit back

Honesty requires the other half. Three new activity types were added to the admin UI, but the Postgres CHECK constraint was never migrated to match — so saves failed, behind a delete-then-reinsert routine whose error branch only wrote to the console. The UI reported success.

That is the tax of no build step and no types: there is no compiler to notice that the values the UI offers are not the values the database accepts. A typed schema shared between both would have caught it.

I still think the trade was right for this project — the decade of maintainability is worth more than the class of bug a type system catches — but the trade is real, and it cost real data before it was found. The fix was an upsert plus an error the interface actually shows.

Available for work

Let's create your next big idea.