Skip to content

Trust

Every claim on this page names a mechanism, not an adjective.

Security pages are usually a list of nouns — encryption, isolation, compliance — with nothing behind them a reader could check. This one is written the other way round. Each claim below names the thing that makes it true: a rule inside the database, a grant that was revoked, a constraint, a test that fails the build. And it ends with a list of what is not done, because a security page with nothing missing from it has not been written honestly.

254Named permissions in the catalogue
138 of 142Tables carrying an organisation column — every one walled off
2Row-level policies written on each of them
13Actions needing a second factor and a written reason
425Tests on isolation, access, approvals and the permission engine
14Database checks that run on every build

Isolation, twice over

Your rows are walled off twice, and the second wall does not trust the first.

Every system that serves more than one company keeps a column on each row saying which company it belongs to, and filters on that column. It works until the day somebody writes one screen out of four hundred and forgets the filter — or adds a table on a Tuesday that nobody remembers to add to the list of tables that need filtering. There is no error when that happens. The screen loads. It just shows somebody else’s camp.

So the filter is written twice, by two different things, and the second one is not the application.

Wall one The application

Your organisation is put onto every read and every write before the query leaves the server, by one piece of code that sits under all of them. It is not something each screen has to remember to do — but it is still code, and code can be worked around by a raw query or a clever join.

invoices.findMany({ where: { status: 'ISSUED' } })
   ↓ the scope extension rewrites it
invoices.findMany({ where: { status: 'ISSUED',
                             orgId: <yours> } })
Wall two The database

Every request opens a transaction that stamps your organisation onto the database connection. Each table that carries an organisation column has a rule saying a row is visible only if its owner matches that stamp. Row security is forced, so not even the table’s owner is exempt, and the application’s own login cannot turn it off.

SET LOCAL app.current_org = <yours>;

CREATE POLICY tenant_isolation ON invoices
  USING (org_id = current_setting('app.current_org'));
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
Six rows asking to come back from one queryillustrative
INV-2026-0401yours
INV-2026-0407yours
INV-2026-0413yours
INV-2026-0503another operator
INV-2026-0508another operator
INV-2026-0505the query that forgot
Three rows are yours, and arrive.Two belong to another operator and never leave the application.One got past it, because that query forgot to filter.

The sixth row is the whole argument. It is the mistake that will eventually be written — a hand-rolled query, a report, a join through a table nobody thought about — and it comes back empty rather than full of somebody else’s tenants. Note also what the answer is: no such invoice, not you may not see that invoice. The second wording would already have confirmed that the invoice exists.

The list of protected tables is not a list

Nothing enumerates the tables that need protecting. A loop walks the database's own catalogue, finds every table carrying an organisation column, and covers it — 138 of the 142 tables in the schema.

A list is a thing somebody forgets to add to. A table created next Tuesday is protected by next Tuesday's migration run, and a separate check in the build re-derives the list and fails if anything was missed.

The stamp evaporates when the transaction ends

The organisation is set with SET LOCAL, which is scoped to the transaction, rather than SET, which is scoped to the connection.

Database connections are pooled and handed to the next request. A stamp that outlived its transaction would be inherited by whoever got that connection next — which is the same leak, arriving from the opposite direction.

The application cannot switch the wall off

The login the application uses owns none of the tables and holds no bypass privilege. A check in every build asserts both.

Row security has an exemption for a table's owner. An application running as the owner has a wall it can walk through by accident, which is a wall with a door in it.

Crossing the wall needs a different login and a flag

Platform-wide reads run on a separate database login and only with an explicit flag set. Naming which logins that bypass rule applies to also took one screen's query from 12.634 ms to 0.042 ms.

A rule left open to every login is OR-ed into every query, and no index can be used against it. That the fix was a hundred-fold speed-up and no change to isolation is the kind of thing you only find by measuring.

Sign-in runs on a login that can reach nine tables

Users, sessions, login attempts, password resets, second-factor enrolments, role assignments, roles, role permissions and organisations. Not a contract, an invoice, an occupant or a property.

Even a total compromise of the sign-in path yields no operational data, because that path was never granted any. Narrowing the blast radius beats trusting the code inside it.

A role can be narrowed to one property, and can expire

Every grant carries a reach — the whole organisation, one portfolio or one property — and may carry an end date. A supervisor scoped to one camp cannot list or open the occupants of another.

Who sleeps at which camp is personal data, not a tidiness problem. And a three-month secondment ends by itself rather than by somebody remembering to end it.

Five database logins, each with the narrowest access it needs
LoginWhat uses itOwns the schemaMay pass the wall
app_migratorMigrations and seedingYesOnly with the platform flag
app_userEvery organisation requestNoNever
app_authSign-in only — nine identity tablesNoNever
app_platformCross-organisation platform readsNoOnly with the platform flag
app_readonlyAnalytics readsNoNever

The one every request runs as — app_user — is the one that can never pass the wall and owns nothing. That is not a configuration setting; it is asserted by the build.

Permissions

A table of ticks is a promise. This is the promise being kept.

Below is one property board — a real shape of screen, with occupancy, contract value, arrears, repair spend and what the operator pays the landlord — rendered as each of seven roles. Change the role and watch the figures that role may not read close in front of you.

Two details matter more than the mechanism. The first is that the figures are withheld, not deleted: a closed tile keeps its label, its unit and its definition, and names the permission that would open it. An empty cell reads as “there is no revenue”. The second is that the withholding happens on the server. A screen can hide a field; it cannot un-send one.

Everything in the organisation, including billing and the danger zone.

Al Quoz Camp 1384 beds · 6 blocks · head lease to 202812 of 12 shown

OwnerThe owner sees the whole board. Nothing here is withheld from them — which is the only reason the other six views are worth looking at.

Pick any figure to see the permission key that governs it, and which of the seven roles hold it.

Withheld, not deleted — the tile keeps its label, its unit and its definition, so nobody mistakes you may not read this for this is empty. And the redaction happens before the JSON is written, not in the markup: a screen can hide a field, it cannot un-send one.

254 permissions, in 15 groups

Every action in the product has its own name and its own one-line description — the sentence you read in the role editor. When you need somebody who can record a cheque but not mark one bounced, that is one checkbox rather than a support ticket.

And when a request is refused, the message names the exact permission missing, so the person can ask for the right thing instead of asking for “access”.

The catalogue, by group
Operations37
Property35
People27
Payments27
CRM24
Billing16
Contracts16
Access15
Landlord13
Expenses12
Organisation9
Pricing7
Reporting7
Comms6
Automation3

Sixteen roles that arrive already configured

Twelve staff roles — Owner, Administrator, Property Manager, Leasing Agent, Finance, Finance Viewer, Site Supervisor, Maintenance, Housekeeping, HR Coordinator, Read-only and Auditor — plus four for corporate clients, residents and landlords. You can hire a camp boss on Sunday and have them working on Monday without designing a permission scheme first.

The Site Supervisor bundle holds zero permissions matching invoice, payment, cheque, deposit, expense, head-lease cost, financial reports or rates — and that sentence is a test, not a paragraph in a manual.

Two dashboards used to return contract values and running costs to that role anyway. The screen hid them. The response did not. Both now withhold the figures before the data leaves the server, and a test drives the endpoint as a supervisor and sweeps the response for the money fields.

Permissions held, by role
Owner254

Everything, including the danger zone and the subscription.

Property Manager227

Full operational and commercial control, within scope.

Finance94

Invoices, receipts, cheques, deposits, refunds, expenses, reports.

Leasing Agent67

Leads, viewings, quotes, contracts, allocation, move-ins.

Site Supervisor57

The ground operation. Not one figure of money.

Not a hierarchy. Finance holds what a repair cost and cannot list a work order; the camp boss sees a document expiry the auditor cannot.

13Actions you cannot take casually

Holding any one of these makes a second factor mandatory on that account. Each act is recorded with a written reason and the name of whoever did it.

Void an invoiceApprove a write-offBackdate a contractBlacklist an occupantForfeit a depositDelete a spaceDeactivate a userAssign rolesManage API keysWrite an automation ruleRun an automation ruleExport all dataClose the organisation

Three of the thirteen guard things this build does not yet do — API keys, a whole-organisation data export, and closing your own account. They are named here because the list is exact, not because the buttons exist.

10Readings that are their own permission

These ten are granted deliberately or not at all. A read-only role does not sweep them up.

The audit trailWhat you pay for a propertyA landlord's full bank accountWhat a repair costOccupant passport and visa numbersA customer's credit fileYour own subscription billingFinancial reportsExecutive reportsThe executive dashboard

A read-only role that reveals the bank account you pay a landlord’s rent into is not a read-only role. The exclusion sits inside the helper that assembles every bundle, so the next sensitive field added is withheld by default rather than by somebody remembering.

Seventeen lifecycle steps ask for their own permission

Submitting a contract is not approving one. Banking a cheque is not marking it bounced. Finishing a job is not signing it off. Seventeen of the thirty-three steps across contracts, cheques and work orders demand a key of their own.

In a market that runs on post-dated cheques, whoever presents a cheque must not also be able to record it bounced — that is precisely the fraud the separation exists to stop.

Backdating is its own dangerous permission

Writing a contract that starts before today needs a key nobody holds by default, judged against your organisation's local date rather than the server's.

Backdating moves revenue into a closed period and rewrites what a tenant owed. Until recently it needed no more authority than fixing a typo, and this page would rather tell you that it was fixed than pretend it was never true.

384 handlers carry a declared permission

Every route states the key it demands, next to the code that runs. A scan in the build counts them and compares the total against the catalogue.

A permission enforced in a comment is not enforced. The count is a ratchet: the number of unused keys may fall and may never rise, so the gap between what the catalogue promises and what the product checks can only close.

Delegation

Nobody can hand out authority they do not hold themselves.

You may grant a role only if you already hold every permission in it. You may edit, reset the password of, or deactivate only somebody whose roles you could grant yourself. The same applies to reach: an administrator confined to two properties cannot grant anybody access to the whole organisation.

The second half is what makes the first half real

Being unable to grant Owner is decoration if you can reset the Owner's password. So acting on a person is gated by the same rule as granting a role.

Otherwise the escalation is one click: reset their password, sign in as them, grant yourself anything.

The refusal names what would have been escalated

A refused grant lists exactly which permissions the person would have gained that you do not hold.

A refusal that says 'not permitted' produces a support ticket. One that names the four keys produces a corrected request.

The organisation can never lose its last owner

Deactivating the last active Owner, or stripping their Owner role, is refused. So is deactivating your own account.

An organisation locked out of itself cannot repair itself — only the platform can restore an owner. This is the mistake somebody makes at four in the afternoon and nobody inside the company can undo.

Approvals

A workflow one person can complete alone is not an approval workflow.

You write the rules: which kind of thing needs agreement, under what condition, and from which role. Eight kinds are modelled — discounts, write-offs, contracts, variations, refunds, expenses, cheque postponements and disciplinary notices — and the conditions read like amount_pct > 10, amount_minor > 500000 or simply always.

The condition is read, never run

A small closed grammar parses the condition. No function calls, no property access, and an identifier the system does not recognise is an error at the moment you save the rule.

Nothing typed into that settings field can reach anything else. And a rule naming a field that does not exist is refused when you write it, rather than silently never firing until an auditor notices a year of unapproved write-offs.

You cannot approve your own request, whatever role you hold

A request you raised is marked as yours and offered a Withdraw rather than an Approve.

This is the rule people assume exists and is usually the first one missing.

The approver needs the authority, not only the desk

A rule that named Read-only or Leasing Agent as the discount approver used to be obeyed. Now the decision also demands the matching permission, so such a rule is refused rather than followed.

The role says which desk. The permission says which authority. A dropdown that let you appoint the person who writes the deal as the person who approves it defeated the whole gate with one click.

An approval is spent, and it is for those exact numbers

Approvals are matched against the figures they were granted for and consumed when used. A 15% discount agreed by a manager does not authorise a 90% one on the next line, and a postponement agreed in March does not re-postpone the same cheque in July.

An approval that becomes a standing exemption is worse than no approval at all.

The queue is filtered on the same key the button demands

Your 'awaiting me' list is built from the permission the Approve button will assert.

A badge counting three items whose buttons both fail is worse than no badge.

What approvals do not do yet

There is no chain — the first rule that catches a request wins, and there is no second step. Nothing escalates a request that is ignored, although the escalation delay is stored and shown.

Stated here rather than in the footnotes, because an operator who assumes a 48-hour escalation exists will find out the hard way that it does not.

The record

An audit trail the software has no permission to edit.

Every change writes who did it, in which organisation, to which record, what action, and the before and after with sensitive values masked — plus the address it came from, the browser and the request id. The application’s database login holds INSERT and SELECT on that table. Update and delete were revoked.

Append-only by grant rather than by convention means it stays true through a bug, through a compromised endpoint, and through somebody deciding to tidy up. The build checks it on every run.

One row, as written illustrative
actor
samir@gulfspace.test
organisation
Gulf Space Management
action
occupant.document.update
record
occupant_documents / 0f3a9c…
before
doc_number ••••4417, expires 2026-11-02
after
doc_number ••••9082, expires 2028-10-30
from
94.200.x.x · Safari · req_8f21c0
at
2026-08-13T09:41:07Z

The masking is the part people miss. An audit row proving that somebody edited a passport number must not itself contain the passport number, or the trail becomes the largest unprotected copy of the data it was written to protect.

Once an invoice is issued, nobody can change it

A draft is editable. The moment it is issued, the amounts, the customer, the number and the dates are frozen — the database rejects the change, not just the screen. Voiding is possible while nothing is paid, needs a written reason, keeps the number, and writes an equal and opposite ledger entry rather than deleting anything.

Your customer has a copy. A gap in your invoice numbering is the first thing a tax authority asks about, and an invoice that quietly changed after it was sent is a document nobody can defend.

Corrections are credit notes, and the ledger only grows

A correction carries a reason code and a written reason, is capped at what is left uncredited, and gets its own number in its own series. The cap is a constraint on the invoice itself — the amount credited may never exceed the amount billed — rather than a rule in the code that writes it.

You keep a trail instead of an eraser. A cap the database refuses to break survives the screen, the import and the next person's bug fix.

Identity numbers are encrypted, and reading one is an event

Passport, visa and ID numbers are encrypted where they are stored, and screens show the last four digits. Seeing a whole number takes a separate permission and a typed reason, and it is written into the audit log against your name and the time.

At the gate the number on the card has to match the number on the record, so the product lets you check it. Everywhere else, a spreadsheet of four hundred passport numbers is not one click away.

Sessions and second factors

A stolen session stops being indefinite access and becomes access that ends when the real person comes back.

Access lasts fifteen minutes and renews itself quietly for up to seven days. Each renewal invalidates the one before it. If a spent renewal is ever presented again — which is what a copied token looks like — the whole line is revoked and everybody signs in fresh. Revocation is checked on every request rather than at the next renewal, so ending a session takes effect now, not in fifteen minutes.

One account, one day illustrative
08:12

Right password. Because this account holds invoice.void — one of the thirteen dangerous permissions — the sign-in stops and asks for a code from an authenticator app.

08:12

The same code, entered twice inside its thirty-second window, is refused. A code glanced over a shoulder stays valid for the rest of that window unless the system remembers it was already spent.

08:13

A fifteen-minute access badge, renewing itself in the background. Each renewal burns the one before it.

14:40

A second device presents a renewal that was already spent. Both devices go dark at once: the whole line is revoked and the real person signs in again.

14:44

Their sessions list shows every device with its address, browser and last-seen time, and a revoke button on each row. Changing the password ends every other session immediately and keeps the one in their hand.

The sign-in lockout ladder
15 min5th
30 min6th
60 min7th
2 h8th
4 h9th
8 h10th
16 h11th
24 h12th

Wrong passwords in a row, and how long the account is locked. Doubling, to a ceiling of twenty-four hours. Every attempt — including guesses at addresses belonging to nobody — is written to a log the application has no permission to edit or delete.

The sign-in form is not a staff directory

Unknown address, wrong password, deactivated account and locked account all return the same answer, after the same amount of work.

Otherwise the login page tells anyone who asks which of your addresses are real, because a wrong guess at a real address takes fifty milliseconds longer than a guess at a fictional one.

Ten recovery codes, stored only as hashes

Issued once at enrolment, single use, and consumed when used. The authenticator secret itself is encrypted in the database, with the key version stored per row so keys can be rotated.

If a copy of the database were ever disclosed, neither the authenticator secrets nor the recovery codes in it would sign anybody in.

Support access60 minutes, read-only by default, in your own records

It takes a written reason to start

Platform support can only reach your organisation through a recorded session, with a reason written down. Write access additionally needs a ticket reference and your organisation's exact name typed out.

Somebody helping you should be able to reproduce your problem, not become your Owner.

It expires, and you can end it

Sixty minutes, a permanent banner while it runs, and it can be ended at any moment from either side.

An open door with no clock on it is not a support session, it is an account.

Even in write mode, twenty-three things stay impossible

A support session can never void an invoice, approve a write-off, backdate a contract, blacklist a person, forfeit a deposit, delete a space, assign a role, read a passport number, read your credit files or audit trail, or close the organisation.

The permissions handed to a support session are built by subtracting all thirteen dangerous keys and all ten sensitive reads — twenty-three in total. Everything not on those two lists, a support session in write mode can do, which is why the record of who looked, when and why is written into your own audit trail: a visit is visible to you, not only to us.

Portals

Three outsiders, one door, and a wrong ID answers “no such thing”.

A resident, the company that houses him, and the landlord the building is leased from all arrive at the same sign-in page and get three different products. Which one you get is decided by the record the account is attached to, read from the database — not by anything the browser asks for.

One account, exactly one party

A portal login is bound to one customer, one occupant or one landlord. A database rule enforces at most one binding, and a request is refused outright if the link is missing or points at a deleted party.

An account that cannot be tied to a party has no rows it may see — and 'none' must never be spelt as 'unfiltered'. In a query builder, an unset filter means everything.

Twenty-seven field names a portal answer may never contain

Portal responses never select the withheld fields in the first place, and the whole isolation suite sweeps every portal response against that list of names.

Protecting a field by removing it from the response after the fact is one forgotten mapping away from disclosure. Never fetching it is one fewer place to be careful.

An operator credential is refused here, and vice versa

The operator workspace, the platform console and the portal issue different token audiences. The same password is not a key to a different door.

These are not three permission sets on one account. They are three separate populations, and a mistake in one cannot become a session in another.

Staff notes on a conversation stay staff-only

An internal note on a message thread is never sent to the portal side of that conversation.

The value of an internal note is that it is internal. A thread that shows both is a thread nobody writes honestly in.

21Portal endpoints behind the party gate
27Field names a portal answer may never contain
44Tests proving one party cannot reach another's data
5 minutesLife of a document download link

Two things portals cannot do, worth knowing before you promise them to a client: there is no payment from the portal — invoices and balances are read-only — and portal users cannot upload anything, so a resident cannot yet upload a renewed visa.

Backups

Rehearsed, rather than assumed.

A backup nobody has restored is a file, not a recovery plan.

Taking a backup is easy and proves nothing. The only thing that matters about a backup is whether it restores, and the only way to know is to restore it — into a database you are willing to lose, and then check that what came back is what went in. So the whole loop runs as part of the build.

The restore rehearsal, step by step

Dump the live databaseRun as a login that can read past the isolation policies — a subtly different role produces an empty dump that looks like a successful one.

Restore into a scratch databaseA fresh database created for the rehearsal and dropped at the end.

Compare row counts, table by tableLive against restored. A table that came back short is caught here.

Checksum eleven tables that carry money and tenancyOrganisations, invoices, invoice lines, ledger entries, payments, payment allocations, cheques, deposit movements, allocations, contracts, audit logs. Equal row counts with different contents is exactly what a half-restored dump looks like.

Confirm the isolation survivedRow security still enabled on every tenant table, and the same number of policies. A restore that lost its policies has all the right data and no isolation at all.

Confirm the triggers and the double-booking rule survivedIncluding the exclusion constraint that stops the same bed being sold twice. If a restore loses it, the system comes back able to double-book every bed it has.

The fifth and sixth steps are what justify the exercise. A dump restores data reliably; what people lose is the surrounding machinery — and losing the isolation policies is a silent breach rather than a visible outage. A database restored without them has exactly the right number of rows and no walls at all.

The rehearsal has caught a real failure

The backup could not read the database. Run as an ordinary application login, the dump fails on tables with forced row security — and a subtly different login would have produced a silently empty dump that looked like a success.

Named here because it is the argument. Nobody would have found that by reading the backup script. It was found by running the restore.

What it does not prove

Point-in-time recovery, write-ahead-log archiving, or that the object store holding the dumps is durable.

Those are deployment concerns rather than repository ones. There is a runbook; there is not yet a proven recovery-time figure to quote you.

Fourteen database checks on every build

Isolation on every organisation table, foreign keys indexed, the audit table still append-only, no bypass rule left open to every login, and the role privileges unchanged.

The failures this catches are the invisible ones: a policy written on the wrong column passes a naive test and protects nothing.

What is not done

A security page with nothing missing from it has not been written honestly.

Everything above is built, and most of it is tested. Everything below is not built, and a few of them are the questions a careful buyer asks first. They are here rather than in a footnote because everything above is only worth reading if this list exists.

Assurance

No third-party penetration test

Nobody outside this project has attacked it. The security work described above is our own, checked by our own tests. An external test has not been commissioned, and until it has, treat this page as a description of intent backed by code rather than as an independent finding.

Assurance

No SOC 2, no ISO 27001, no formal certification

There is no audit report to send your procurement team. If a certificate is a hard requirement for you, this is not yet the product for you, and we would rather you learned that here than in week six.

Resilience

One region, and no point-in-time recovery

The restore rehearsal proves the dump-and-restore pair. It does not prove write-ahead-log archiving, point-in-time recovery, or that the store holding the dumps is durable. There is no second region and no documented failover time.

Access

No single sign-on

There is no SAML or OpenID Connect. Nobody signs in with your company identity provider, there is no domain-based auto-provisioning and no group-to-role mapping. Accounts are created in the product and carry their own password and second factor.

Access

You cannot build a custom role

Sixteen roles ship configured and you assign and revoke those. The permissions to create a role are in the catalogue with no screen behind them, so a bundle that is nearly right cannot yet be edited into one that is exactly right.

Access

No organisation-wide "everyone must use a second factor"

A second factor is mandatory for accounts holding one of the thirteen dangerous permissions, and optional for everybody else. You cannot yet require it of the whole company.

Access

No rate limiting on sign-in

The per-account lockout ladder above is real and works. There is no throttle in front of it, on the sign-in endpoint or on export and notification endpoints, so an attacker spreading guesses across many addresses meets the ladder but not a rate limit.

Access

The compromised-password check is a short deny-list

Fifteen obvious long passwords, bundled. Not the top hundred thousand, and not a range query against a breach corpus, both of which the specification asks for.

The record

There is no audit-trail screen for operators

Every change is written, masked, and cannot be edited or deleted — that part is real. But there is no per-record Activity tab in the operator app and no export of the trail. Audit rows are readable through the platform console only, which means today you would ask us for them.

The record

The audit table is not partitioned, and nothing prunes it

It grows for ever. There is no monthly partition, no retention job and no archive-and-drop.

Approvals

Nothing escalates an approval that is ignored

You can set "escalate after 48 hours" and it is stored and displayed. No job acts on it, so an unapproved request waits until somebody looks at the queue.

Approvals

One rule decides, and there is no chain

The first rule that catches a request wins. There is no two-step ladder, no "manager then director", and no parallel approvers.

Approvals

Four of the eight approval kinds have nothing raising them yet

Discounts, write-offs, contract variations and cheque postponements do raise approvals. Contracts, refunds and expenses are modelled but nothing calls them, and the disciplinary approval is raised only by an eviction notice.

Data rights

No per-person export, no erasure, no organisation export

You cannot yet produce everything held about one occupant, erase or anonymise them, or take a full copy of your organisation out on your own. Ask us and it is a manual job.

Delivery

Nothing sends until you configure your own mail relay

Notifications and alerts are composed, suppressed, de-duplicated, digested and logged. With no relay configured, the log records the attempt and the reason rather than a failure that looks like a success. The delivery guarantees are your provider’s, not ours.

Coverage

53 of the 254 permissions are declared and demanded by nothing

Among them the role-editing keys, API keys, the audit export, most block, floor and unit verbs, and a handful of billing verbs. A test in the build records that number and fails if it rises, so the gap can shrink and cannot quietly grow — but today those keys govern nothing.

Coverage

No API keys, no webhooks carrying real events

A dangerous permission exists for API keys with no endpoint behind it. Webhook endpoints can be registered and a signed test delivery sent, but no business event is ever published to one.

Coverage

A permission change does not reach an open browser instantly

The server is stricter than the specification asks — permissions are re-derived from the database on every single request, so a revoked grant stops working immediately. But there are no realtime events anywhere in the product, so the screen in front of the person learns about the change from a response header rather than being pushed.

One thing on this list is a deliberate difference from the specification rather than a gap: permission changes are not pushed to open browsers. The server is stricter than asked — permissions are re-derived from the database on every single request, so a revoked grant stops working immediately rather than at the next refresh. What is missing is only the nudge that tells the screen to redraw.

Before you start

Ask the hard questions now, not in month three.

Ask what happens to a passport number when an occupant leaves. Ask what an auditor can read and what they cannot. Ask for the exact wording of the refusal a supervisor gets when they open a camp that is not theirs. Ask us to restore last night’s backup into a scratch database while you watch.

Every answer on this page is a mechanism you can be shown rather than a claim you have to take on faith — and where there is no mechanism, it is on the list above. That list will get shorter. It will not get quieter.