Skip to content

Control who sees what

Access in MatterData is declared, not programmed. You write the rule on the page, and the engine puts it into the SQL that reads the rows. There is no place in an app where you can forget the check, because there is no place where you write it.

This guide covers three levels: who can open a page, which rows they see, and which columns.

Every page declares the role that can open it. A page with no rule admits nobody.

page members "Members" {
route: /members
access: board
report of members "All members" {
columns: name, email, joined_on
}
}

Two reserved role names can be written in a rule. public admits anyone, including a visitor with no account. authenticated admits anybody who holds a role in this application.

access: public

authenticated is not “has a session”. An organization can hold several applications, and a login with no role in yours is anonymous in yours. Read An organization, its applications and one login.

Three more names are reserved and are refused in a rule and in a declaration: owner, anonymous, and tenant_admin. See Reserved names.

A request from a role that is not admitted gets a 404 and never a 403. A 403 confirms that the page exists.

Add where to the rule. The condition is about a row of the page’s table.

page my_notes "My notes" {
route: /notes
access {
office
author where owner = :user.id
}
report of notes "Notes" {
columns: title, released, updated_at
}
}

The office sees every note. An author sees the notes she owns. Both use one declaration and one URL.

:user.id is the signed-in user. The engine resolves it once per request, before it renders anything.

The condition becomes part of the query. It is not a filter applied to rows that were already read, so a row that the rule excludes never leaves the database.

A condition can walk a reference, up to four steps.

access {
teacher where course.teacher = :user.teacher_id
}

A region can carry its own rule. The region’s rule narrows the page’s rule and can never widen it.

page overview "Overview" {
route: /overview
access: authenticated
report of notes "Public notes" {
columns: title
where: released
}
form of notes "Add a note" {
access: office
fields: title, body
actions: save
}
}

An author opens the page and sees the report. The form is absent for her, and not refused. A body that names the form anyway stores nothing.

Mark the column sensitive. After that, only a role whose rule grants the column can read it.

table grades "Grades" {
display: student
student ref(students) required "Student"
points int sensitive required "Points"
remark text sensitive "Remark"
}
page grade_sheet "Grades" {
route: /grades
access {
head_teacher { grants: grades.points, grades.remark }
teacher where course.teacher = :user.teacher_id { grants: grades.points }
}
report of grades "Grades" {
columns: student, points, remark
}
}

The head teacher reads both columns. The teacher reads the points and not the remark. For the teacher the remark is absent: no cell, no heading, and no name that a request can use.

The same grant decides writing. A form renders a control, and a sheet an editable cell, for a granted column only. A request that names an ungranted column is ignored and logged.

A column derived from a sensitive column must be marked sensitive too. The compiler refuses one that is not, because an aggregate over one row is that row’s value.

These hold for every app, and you do not write them.

  • A row that the reader cannot see returns the same 404 as a row that does not exist.
  • The row rule applies to writes. An invisible row cannot be updated or deleted.
  • The row rule applies to exports. A CSV file contains the rows the reader can see.
  • Every form that changes data carries a CSRF token.
  • SQL is always parameterized. A column name that arrives in a request goes through an allowlist.

Access is asserted by a generated matrix over every route and every role, including anonymous. A new route with no rule fails that test. When you change a rule, the matrix is where you see what changed.

For what the engine enforces around your rules, and the three places it needs your discipline, read Security model.