Request path
Five layers, one rule.
A request passes through each of these. The rule does not change between them: nothing is trusted because of where it came from.
The browser
React, client-rendered. Every query it makes carries the signed-in user's own token — it has no elevated credential to lose and no privileged path to abuse.
Permission checks in the interface are cosmetic. They hide controls; they do not protect data.
The policy layer
PostgreSQL row-level security. Every table carries its organisation, and every policy keys on the organisation the caller belongs to. Switched on by an event trigger when a table is created.
A table nobody wrote policies for is invisible to everyone, rather than open to anyone.
The functions
Anything that contends for a resource or grants an authority runs as a SECURITY DEFINER function: holding a unit, issuing a library copy, waiving a fine, releasing a child to an adult.
A check followed by a write, from a browser, is two statements with a gap between them.
The edge
Deno functions, used only where a service-role key is unavoidable — creating logins, signing uploads, receiving from devices. Each re-derives the caller from their token and re-checks their role before acting.
Never trusts anything in the request body about who is asking.
Storage
Cloudflare R2. A private bucket behind ten-minute signed URLs for records; a separate, physically distinct public bucket for anything intended to be seen by strangers.
A path-handling mistake in a shared bucket is how a private document becomes a public URL.
Isolation
One database per vertical. One organisation per row.
Schools and property firms do not merely have separate tables — they have separate Postgres projects. Nothing joins across them because there is nothing to join to.
Composite keys
A row that references two other rows must reference two rows belonging to the same organisation. The key carries the organisation, so the wrong combination is a constraint violation rather than a bug.
Separate projects
Each vertical has its own database, its own credentials and its own migration history. A mistake in one cannot reach the other, because there is no connection between them to misuse.
Enforced imports
Vertical code may import shared code. Shared code may never import a vertical, and one vertical may never import another — checked by a lint rule, not by convention.
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.
Support access
We can be held to it too.
Somebody at Orchestrix occasionally needs to look inside a customer’s data to fix something. That is a real requirement and pretending otherwise produces a worse system, not a safer one.
So it is explicit. Entering a customer’s organisation is a recorded action. The session carries a timestamp and expires on its own. Where a customer requires it, access needs their approval first, and the approval is per-request.
A support session that never ends is one forgotten browser tab away from an accidental write in somebody else’s business.
Technology
Every layer, and why.
| Layer | Choice | Reason |
|---|---|---|
| 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. |
Delivery
Tested where it counts.
The suite runs against a hand-written mock that reproduces the database’s own rules — the constraints, the refusals, the allocation order. A test that only proves the screen rendered is a test that passes while the money is wrong.
Every request is intercepted, so the suite needs no database, no credentials and no internet. It runs the same on a laptop as it does in CI, which is the only way a test suite stays trusted.