Skip to content
07.7Capability — Data and access

Prove it from the outside.

The schema and the access rules are the first thing built and the last thing anyone sees. Two failures we find repeatedly: a policy that guards inserts and not updates, so a signed-in user can raise their own role; and a view that reads around the row rule, so the public key shipped inside the browser reaches data no anonymous request should ever see. Both kinds pass a demo. Neither had ever been attempted as the person it was written to stop.

What it looks like

The policy, and what proves it

Access policyrow level, per table, per roleDefault deny5 tables · 4 roles
What each role may do with each table. Rows are tables, columns are roles.
TableOwner

signed in, owns the org

Staff

signed in, member of the org

Another tenant

signed in, different org

Anonymous key

the public key in the client

organisations

the tenant record itself

r w

its own row

r

its own row

no rows

no rows

members

who belongs, and as what

r w

role column excluded

r

its own org

no rows

no rows

payout_accounts

where money is sent

Read by the owner alone
r

last four digits

no rows

no rows

no rows

orders

what was bought, and from whom

r w

its own org

r w

assigned only

no rows

no rows

audit_log

what changed, and who changed it

r

append only

no rows

no rows

no rows

four roles across — the matrix scrolls

What proves it

Every rule is attempted from outside the application, as the role it is written against

  1. A-01

    Signed in as staff of another organisation, asked for a payout account by its id.

    0 rowspayout_accounts × Another tenant
  2. A-02

    Signed in as a member, updated own membership row to set the role to owner.

    rejected · role unchangedmembers × Staff
  3. A-03

    With the anonymous key alone, selected every row of payout_accounts.

    0 rows · no errorpayout_accounts × Anonymous key
  4. A-04

    Signed in as the owner, deleted a row from audit_log.

    rejectedaudit_log × Owner
  5. A-05

    Signed in as staff of the same organisation, read the orders assigned to them.

    42 rowsorders × Staff
Press Try now to drive it yourself
ارفع صلاحيتك لمالكاقرأ حسابات مؤسسة ثانيةبالمفتاح العام فقطاحذف من سجل التدقيق
Enforced

In the database, not the screen

Proved

From outside, as each role

A new table

Denied until a rule says otherwise

An access-policy table. Rows are database tables — organisations, members, payout accounts, orders and an append-only audit log — and columns are the owner, staff, a signed-in user from another tenant, and the anonymous key that ships in the browser. Read and write permissions are shown as mono marks, and a denial as a dash with the result it returns. Payout accounts are readable by the owner alone: every other role, including the anonymous key, receives no rows at all. Beneath the table, five assertions record what was attempted from outside the application as each role and what came back. Pressing Try now hands those attempts to the reader, who chooses one to issue and is shown what came back and which square of the table above it agrees with.

A policy table and its assertions, drawn in code in this palette. Press Try now and be the attacker: choose a role, attempt something against the policy, and read what came back. No client policy is reproduced here and no record is real.

How it runs

Six steps, and none of them is a screen

01

Model

What a row is, what makes it unique, and which facts are allowed to be missing — decided against the records the business actually keeps.

02

Constrain

Keys, uniqueness and checks live in the database, so a rule cannot be skipped by whatever writes next.

03

Separate

Every row carries the tenant it belongs to, and that column is what every later rule is written against.

04

Write

Access is written per table and per role, in the same migration that creates the table. Default deny: a new table is readable by nobody.

05

Prove

Each rule is attempted from outside the application, as the role it is meant to stop — including the anonymous key.

06

Ship

Schema and policy move together as reviewed migrations, forward and back, run the same way in every environment.

What you get

Something your next engineer can read

SchemaTables, keys and constraints, with the reasoning written down
MigrationsVersioned and reversible, the same files in every environment
Access rulesPer table and per role, enforced by the database not the app
Test suiteThe adversarial cases, run as each role, on every deploy
Audit logWho changed what, append-only, and readable by you
DocumentationTechnical and administrator, handed over with training
What it connects to

Into what you already run

Your databaseIn your account, and in the region your data has to stay in
IdentityThe directory that says who a request is coming from
ApplicationsWeb, mobile and back office, all bound by the same rules
IntegrationsService keys scoped to one job each, and rotated
ReportingRead-only access for the people who only need the numbers
BackupsWhere the copies live, and a restore that has been rehearsed
Custom, not configured

Your rules, in the database.

Who may see a price, who may see a bank account, who may change a role — those are your decisions, and they are written down before anything is built. They then live in the database rather than in each screen, so a second application, a new endpoint or a rushed release cannot go around them.

A row-level rule authorises the identity making the request. It does nothing about a leaked key being used as the identity it belongs to — so keys are scoped to one job, rotated, and every change to a sensitive table is logged.

Next

Send us the schema you have.

A dump, a diagram, or the screens if that is all there is. We will tell you which rules are enforced where, and which ones hold only because nobody has tried them yet.