Schools
K-12 schools running admissions through to alumni on one system.
The whole institution — the classroom, the office, the bus and the hostel — with a parent portal that is a different product on the same data.
28 modules
ExploreThe problem
Not with an error. With a payroll run that used a month of unmarked attendance. With a flat sold twice because two people pressed the button in the same second. With one school’s report card rendering another school’s name.
None of those raise an error. They tend to surface weeks later, usually in front of somebody who matters.
So we try to make those states simply not possible — caught by a constraint, a lock or a policy, rather than relying on everyone to remember.
What we run
Each vertical has its own database, its own vocabulary and its own screens. What they share is the part that must never be wrong.
K-12 schools running admissions through to alumni on one system.
The whole institution — the classroom, the office, the bus and the hostel — with a parent portal that is a different product on the same data.
28 modules
ExploreDevelopers, plotted-land firms and interior studios.
Enquiry to registration: the pipeline, the inventory, the price, the booking, the schedule and the money that arrives against it.
17 modules
ExploreClinics and small hospitals.
The third vertical. Same spine, same isolation model, a different operational vocabulary.
Still in designTenant isolation
Every table carries the organisation it belongs to. Every policy keys on the organisation the caller is in. Row-level security is switched on automatically when a table is created, so a table nobody wrote policies for is invisible to everyone rather than open to anyone.
When a row references two other rows, both must belong to the same organisation — enforced by a composite key, not by the code that inserts it.
One deployment, many organisations
Every table carries an organisation. Every policy keys on it.
No cross-tenant reference is possible
Composite keys make the wrong row a constraint violation.
Vendor access is recorded
Entering a customer's data is logged, and the session expires.
No biometric template ever leaves a device
A punch is an enrolment number and a time. A fingerprint cannot be reissued.
Pickup codes are unreadable
The code that collects a child has no select policy at all.
Deletion is archival first
Nothing is destroyed in one step, and the second step is narrow.
How we build
Each of these came out of something we learned the slower way, and each is now enforced by the system rather than by memory.
Row-level security, not application checks
Every query runs in the browser under the signed-in user's own token. There is no server-side API layer to trust, and that is deliberate: the only thing standing between one organisation's data and another's is a Postgres policy, which is the same code path for every client, every screen and every mistake.
A permission check written into a screen protects that screen. The next one has to remember.
Composite foreign keys, not two independent ones
When a row points at two other rows, both must belong to the same organisation — enforced by a composite key on (id, org_id), not by the code that inserts it. Independent foreign keys permit a booking that references one firm's unit and another firm's customer.
It is better for the database to reject a cross-tenant row than for every future feature to remember not to write one.
SECURITY DEFINER for contention and authority
Two salespeople holding the same flat, two librarians issuing the same copy, a fine being waived, a child being handed to an adult — none of these are client-side writes. They are database functions that take the lock, check the rule and record who used them.
A check and then a write, from a browser, leaves a gap in between — and that gap is where two people get the same flat.
Enforced in the schema, not in a screen
If one module's correctness depends on another writing a column, a trigger does it. Student class is mirrored between two tables in both directions, so the students screen and the promotion wizard each work without knowing the other exists.
A contract that lives only in someone's head is easy to lose track of as the codebase grows.
Not against what the code implies
Reading an application's types tells you what it uses, never what the database actually has. Migrations written from inference have produced wrong helper names and indexes that were quietly missed.
A migration written from assumption can report success while quietly leaving the problem in place.
A price agreed is a price recorded
A booking copies its unit price, its charges and its discount at the moment it is made. Repricing the tower next month does not reach backwards. The figures a buyer signed are stored, not derived.
A total that recalculates will eventually disagree with a document somebody has already signed.
Technology
Every layer below is here for a stated reason. Anything that could not justify itself is not in the list.
| Layer | Choice | Why this one |
|---|---|---|
| Application | Next.js · React · TypeScript | Client-rendered throughout. There is no server tier to compromise because there is no server tier. |
| Data | PostgreSQL via Supabase | Policies, triggers, constraints and functions carry the rules. One database per vertical — separate projects, no shared rows. |
| Authorisation | Row-level security | Enabled by an event trigger on table creation, so a table without policies is invisible rather than open. |
| Server work | Deno edge functions | Only where a service-role key is unavoidable: creating logins, signing uploads, ingesting from devices. Each re-derives the caller from their token before acting. |
| Files | Cloudflare R2 | Private bucket behind short-lived signed URLs for records; a separate public bucket for anything meant to be seen. |
| Payments | Pluggable adapters | Razorpay, Cashfree, PayU, Stripe. Provider-agnostic on the application side; per-tenant secrets encrypted at rest. |
| Mobile | PWA · Capacitor | Installable on any phone, and a native Android build from the same static export. |
Scope
Everything listed is in production use. If something is still being built, you will not find it here yet.
The hard parts
Feature lists are easy. These are the two problems that took the most care to get right.
Leave accrues on an academic year, April to March, pro-rated for mid-year joiners, and never for a period before the school was onboarded. Payroll blocks on unmarked attendance inside the employment period. Neither rule is a preference. Both are there because a salary is a number someone has to stand behind when a teacher asks about it.
A receipt does not pay a demand — it is allocated against demands, in amounts, as rows. That one shape handles a part payment, a single cheque across three demands, and an overpayment with nowhere to go, without a special case for any of them. TDS is carried separately, so that above the threshold the buyer is credited in full even though the developer banked one percent less.