Nirmanam
"A ledger a builder can trust and a supervisor can actually use."
Table of Contents
What it is
Construction is one of the biggest industries in the world and one of the least digitized. Most builders still run their finances on paper, WhatsApp messages, and a mental model of "who owes what." Money leaks through those gaps, a receipt lost here, an advance forgotten there, and nobody finds out until the job is done and the numbers don't add up.
Nirmanam (നിർമ്മാണം) is Malayalam for "construction." The name is the product: one calm place to track every rupee that moves in and out of a project, who spent it, and why.
There are two roles, and the product is built around the reality that they're very different people:
- Builders own sites. They create projects, set budgets, manage expense categories, invite their team, and review reports and an audit trail.
- Supervisors run the day-to-day on the ground. They only see the sites they're invited to, and they can add ledger entries and notes, but they can't edit or delete history, and they can't touch the books.
That permission boundary matters more than any feature. On a construction site, trust is the product. The builder needs to know that a supervisor can log an expense but can't quietly rewrite the ledger afterwards.
How it works
A builder stands up a site and invites supervisors to it. Supervisors log entries from the ground; each insert is re-validated by the API, mirrored into an append-only audit trail, and any receipt lands in a private bucket behind one-hour signed URLs. The builder gets per-site summaries, monthly breakdowns, expense-by-category bars, and a full activity log. Archiving a site hides it and locks it read-only, with a warning if the books don't balance.
Screenshots
Architecture Decisions
Why two layers of authorization?
> Every table has row-level security in Postgres, but the backend talks to Supabase with a service-role key that bypasses RLS. So the API re-checks every access rule itself.
Two fences, not one. If someone finds a way around the API, the database still holds. The rule is simple and never violated: a supervisor can insert, but never update or delete.
Why soft deletes and an append-only audit log?
> Deleting a ledger entry doesn't remove it. It timestamps deleted_at.
Every mutation writes an audit row server-side (with the service key), so a user can't fabricate their own history. The builder sees who did what, forever.
Why private receipts via signed URLs?
> Financial documents should never be publicly readable, even by URL guess.
Uploaded files go into a private Supabase Storage bucket and come back as one-hour signed URLs. Paths are scoped to the site and entry.
Why a serverless FastAPI on Vercel?
> The workload is CRUD + reporting. No long-running workers, no WebSockets.
The FastAPI app runs as a Vercel Python function, which is honest about what it is. The CSV export streams instead of buffering a giant file in memory.
> Tradeoff: no background jobs on this host. Everything happens inside the request lifecycle.
Why white-labeling is a build-time variable?
> Brand name, logo, and accent color are env vars, not database rows.
The whole UI (buttons, nav, focus rings, the auth screen) derives from one accent hex. Point it at a new customer and it becomes their app, not mine.
Why feature flags on every surface feature?
> audit_log, categories, notes, signup can each be disabled with a JSON blob in an environment variable.
Flags are enforced in both the API and the UI. A customer who doesn't want public sign-up just gets a login screen with no "Create account" link.
Features
- Sites:name, location, client, budget, and status per project; archive (hidden and read-only) and reactivate, with a balance warning before closing the books
- Ledger:income and expenses with category, payment method, date, description, optional note, and a privately stored receipt served via short-lived signed URLs
- Categories:a shared set of expense/income categories maintained by the builder across all their sites
- Notes:standalone site notes plus context attached to individual entries
- Reports:per-site summary, monthly breakdowns, expense-by-category bars, and streaming CSV export
- Activity log:append-only audit trail of everything that changed, visible only to the builder
- White-labeling:brand name, logo, and accent color from build-time variables
- Feature flags:
audit_log,categories,notes,signuptoggleable per customer via env JSON, enforced in API and UI
Agent-First Operations
The part that made this a product, not a project: the whole thing is agent-first. There's an AGENTS.md playbook and a set of scripts (rebrand, deploy-local, deploy-cloud) that let an LLM agent stand up a fully branded instance (new Supabase project, migrations, seeded admin, Vercel deploy, the works) from a three-sentence conversation.
The short version: the person running this doesn't need to know what a migration is. I wrote the full approach in Agent-First Applications.
Tech Stack
- Frontend: React + Vite (SPA with build-time theming)
- Backend: FastAPI on a Vercel Python function (streaming CSV export)
- Auth / Database / Storage: Supabase (Postgres RLS, private buckets, signed URLs)
- Hosting: Vercel (cloud) or Docker Compose with local Supabase (self-hosted)
- Customization: env-driven white-labeling + JSON feature flags enforced in API and UI
- Operations: AGENTS.md playbook + idempotent rebrand/deploy scripts driven by an LLM agent
Key Numbers
- 2 roles:builders (own the books) and supervisors (insert-only)
- 2 authorization layers:API re-checks on top of Postgres RLS
- 4 feature flags:
audit_log,categories,notes,signup - 1 hour:signed URL lifetime for private receipts
- 3 deploy scripts:
rebrand,deploy-local,deploy-cloud - 3 sentences:enough for an agent to stand up a fully branded instance
- 2 deployment targets:Docker Compose locally, Vercel + Supabase in the cloud
- INR only:every amount is a rupee, because the site is the bank
What I learned
- Trust is a feature. The insert-only supervisor boundary shaped the schema, the API, and the UI more than any single capability did.
- Defense in depth pays. Row-level security plus API re-checks sounds redundant until you remember the service-role key bypasses RLS by design.
- Boring stacks ship. React + FastAPI + Supabase has no novelty, which is exactly why the interesting decisions (audit trail, signed URLs, flags) got the attention they needed.
- White-labeling is a branding problem only if you make it one. One accent hex and two env vars turned a personal project into a sellable product.
- Building with an agent from day one changes what you build. The operational surface (scripts, idempotency, no secrets in source) fell out of the dev workflow for free.