Engineering

Six principles, and how we arrived at them.

Less a manifesto than a set of habits. Each came from experience, and each is enforced by the system rather than by everyone remembering.

6principles
6considered trade-offs
0we skip under pressure
01

The database is the boundary

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.

Otherwise

A permission check written into a screen protects that screen. The next one has to remember.

02

Two references, one tenant

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.

Otherwise

It is better for the database to reject a cross-tenant row than for every future feature to remember not to write one.

03

Anything that races is a function

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.

Otherwise

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.

04

Features do not depend on memory

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.

Otherwise

A contract that lives only in someone's head is easy to lose track of as the codebase grows.

05

Verified against the running database

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.

Otherwise

A migration written from assumption can report success while quietly leaving the problem in place.

06

Frozen means frozen

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.

Otherwise

A total that recalculates will eventually disagree with a document somebody has already signed.

Trade-offs

Choices we made deliberately.

Every principle has a cost somewhere. These are the trade-offs we chose, and what each one gave up.

No LLM in the assistant

The in-app assistant matches intent deterministically and runs a read-only query, so every answer is scoped by the same policies as the screen it came from. It costs nothing per message and cannot invent a number.

No shared database between verticals

It would be cheaper to run one. It would also mean a mistake in the school code could reach a property firm's data, and no amount of care makes that acceptable.

No biometric templates, ever

Gate scanners send an enrolment number and a timestamp. A password can be changed after a breach; a fingerprint cannot.

No hard delete in one step

Archive, then delete, and the second step is narrow and refuses anything that is not already archived. A receipt number that vanishes is exactly what an auditor asks about.

No cost per message for WhatsApp

Click-to-send links rather than an API. A pre-filled message opens on the sender's own account — no gateway, no per-message fee, no third party holding the conversation.

No feature flag that grants itself

Paid modules are absent-means-off, so a newly provisioned customer never receives a billable module by accident. Everything else is absent-means-on.

Working method

Small changes, clear failures.

A change that touches one module should not require another module to be updated in step. Where that coupling is unavoidable, it lives in the database as a trigger or a constraint — somewhere that cannot be forgotten — rather than in a screen that must remember.

Migrations are verified against the running database, never against what the application’s types imply. Reading the code tells you what the code uses; it does not tell you what the schema has.

When something is broken, it should fail where it is wrong. A silent fallback that produces a plausible number is worse than an error, because a plausible number gets printed and posted.

And when we are not sure of something, we would rather say so than present a confident guess.

If this sounds like your kind of team, we would like to hear from you.

Get in touchSee the architecture