Example: a knowledge base
Documentation storage for organizations: company pages, team pages that are private, shared with the organization or public, and drafts. A tested realm.
A place where organizations keep their documentation: the company’s pages (a handbook, policies), and each team’s own, which the team decides to keep to itself, share with the rest of the organization or publish to the world. The whole realm is examples/config/kb, and its rules are tested with backd rules test.
Who is who
| Who | Is | Can |
|---|---|---|
| Olga | owner of the organization | write the company’s pages; read every published page |
| Ana | member of Platform, editor there | write, publish and decide who reads Platform’s pages |
| Ben | member of Platform | read Platform’s pages, the organization’s and the public ones |
| Carla | member of Support, contributor in Platform for two weeks | start drafts and edit the text of Platform’s pages |
| Dan | member of Support | read what is shared with the organization and Support’s pages |
| A visitor | nobody, no account | read the public pages |
The realm
examples/config/kb/realm.yaml
# The documentation store of docs/organizations/example-knowledge-base: each organization keeps
# its pages, its teams keep their own, and every page says who may read it.
auth:
strategies:
password: [password]
signup: open
organizations:
roles:
owner: { description: Runs the organization and its company pages, manage: [org, members, roles, teams] }
member: { description: Everyone in the organization }
default_roles: [member]
# Roles held within one team.
team_roles:
editor:
description: Writes, publishes and decides who can read the team's pages
grants: [contributor] # may hand writing to someone else, in this team only
require_mfa: true
contributor:
description: Writes drafts and edits the team's pages, usually for a periodeditorrequires a second factor: without one it only counts as a plain member.editorcan grantcontributor, in its own team.- There is no role for “reads the team’s pages”:
inTeamsays it.
The collection and its rules
examples/config/kb/main/pages/collection.yaml
# A page belongs to an organization, and to a team when it is the team's: a page with no team is
# the organization's (a handbook, a policy). `visibility` says who reads a published page:
# team the team's members, editors and contributors (and nobody else, for a company page)
# org every member of the organization
# public anyone, signed in or not
organization: required
on_org_delete:
action: delete
team: optional
on_team_delete:
action: keep # the pages stay, with the old team id, readable by their visibility and the owners
rules:
# A draft is its author's alone. A published page is read by its visibility; owners read every
# published page of the organization.
read: >
(user != nil && document._meta.owner == user.id)
|| (document.status == 'published' && (
document.visibility == 'public'
|| (document.visibility == 'org' && inOrg(document._meta.org))
|| inTeam(document._meta.team)
|| hasTeamRole(document._meta.team, 'editor')
|| hasTeamRole(document._meta.team, 'contributor')
|| hasOrgRole(document._meta.org, 'owner')
))
# Owners write the company's pages; editors write their team's; contributors only start drafts
# that only the team sees.
create: >
user != nil && (
(data._meta.team == nil && hasOrgRole(data._meta.org, 'owner'))
|| hasTeamRole(data._meta.team, 'editor')
|| (hasTeamRole(data._meta.team, 'contributor') && data.status == 'draft' && data.visibility == 'team')
)
# Editors (and owners, for company pages) change anything, publishing and visibility included.
# Contributors and authors edit the text and leave status and visibility as they are.
update: >
user != nil && (
(document._meta.team == nil && hasOrgRole(document._meta.org, 'owner'))
|| hasTeamRole(document._meta.team, 'editor')
|| ((document._meta.owner == user.id || hasTeamRole(document._meta.team, 'contributor'))
&& data.status == document.status && data.visibility == document.visibility)
)
# Authors discard their own drafts; editors and owners remove pages.
delete: >
user != nil && (
(document._meta.owner == user.id && document.status == 'draft')
|| (document._meta.team == nil && hasOrgRole(document._meta.org, 'owner'))
|| hasTeamRole(document._meta.team, 'editor')
)- A page with no team is the organization’s.
team: optionallets the same collection hold the company’s pages and each team’s. A team page is created with?team=. visibilitydecides who reads a published page:team(its members, editors and contributors),org(every member of the organization) orpublic(anyone, signed in or not). Owners read every published page of the organization.- Drafts are the first line of
read: whoever created a page reads it whatever its status, and nobody else reads a draft, owners included. Publishing is changingstatus, which only editors (and owners, for company pages) do. readis filtered inside the query: Dan’s list never contains Platform’s private pages, so nothing is received to be hidden later.update: contributors and authors edit the text and leavestatusandvisibilityas they are.deletelets authors discard their own drafts.on_team_delete: keep: when a team is deleted its pages stay, with the old team id, readable by their visibility and by the owners.
Giving and ending cover
POST /v1/acme/_orgs/{org}/teams/{platform}/roles # as Ana
{ "user": "<carla>", "role": "contributor", "from": "2026-08-01T00:00:00Z", "until": "2026-08-15T00:00:00Z" }
From 1 August Carla’s lists include Platform’s pages and she can write drafts there; on the 15th that is over, with nothing to clean up. Ana can end it earlier with DELETE on the same path.
The tests
examples/config/kb/main/pages/rules.test.yaml
# Who reads and writes what: the visitor, a plain member, another team's member, a contributor from
# another team, an editor and an owner.
users:
ana: { id: u1, email: ana@acme.example, orgs: { acme: { roles: [member], teams: [platform], team_roles: { platform: [editor] } } } }
ben: { id: u2, email: ben@acme.example, orgs: { acme: { roles: [member], teams: [platform] } } }
dan: { id: u3, email: dan@acme.example, orgs: { acme: { roles: [member], teams: [support] } } }
# Helps Platform from Support: holds contributor there, without belonging to it.
carla: { id: u4, email: carla@acme.example, orgs: { acme: { roles: [member], teams: [support], team_roles: { platform: [contributor] } } } }
olga: { id: u5, email: olga@acme.example, orgs: { acme: { roles: [member, owner] } } }
documents:
policy: { title: Time off policy, body: "...", status: published, visibility: org, _meta: { owner: u5, org: acme } }
runbook: { title: On-call runbook, body: "...", status: published, visibility: team, _meta: { owner: u1, org: acme, team: platform } }
guide: { title: Deploy guide, body: "...", status: published, visibility: org, _meta: { owner: u1, org: acme, team: platform } }
api: { title: Public API reference, body: "...", status: published, visibility: public, _meta: { owner: u1, org: acme, team: platform } }
draft: { title: Incident template, body: "...", status: draft, visibility: org, _meta: { owner: u1, org: acme, team: platform } }
faq: { title: Support FAQ, body: "...", status: published, visibility: team, _meta: { owner: u3, org: acme, team: support } }
tests:
- name: a visitor reads only what is public
as: anonymous
read: { allow: [api], deny: [policy, runbook, guide, draft, faq] }
- name: a member reads the organization's pages and their team's, not another team's private ones
as: ben
read: { allow: [policy, runbook, guide, api], deny: [draft, faq] }
update:
- { document: runbook, patch: { body: "edited" }, expect: deny }
create:
- { data: { title: Notes, body: "...", status: draft, visibility: team }, org: acme, team: platform, expect: deny }
- name: another team's member reads what is shared with the organization, not the team's private pages
as: dan
read: { allow: [policy, guide, api, faq], deny: [runbook, draft] }
- name: an editor writes, publishes and decides who reads, in their own team
as: ana
read: { allow: [policy, runbook, guide, api, draft], deny: [faq] }
update:
- { document: draft, patch: { status: published }, expect: allow }
- { document: runbook, patch: { visibility: public }, expect: allow }
- { document: faq, patch: { body: "edited" }, expect: deny }
- { document: policy, patch: { body: "edited" }, expect: deny }
create:
- { data: { title: Handbook, body: "...", status: published, visibility: org }, org: acme, team: platform, expect: allow }
- { data: { title: Handbook, body: "...", status: published, visibility: org }, org: acme, team: support, expect: deny }
- { data: { title: Handbook, body: "...", status: published, visibility: org }, org: acme, expect: deny }
delete: { allow: [runbook, draft], deny: [faq] }
- name: a contributor from another team writes drafts and edits the text, but publishes nothing
as: carla
read: { allow: [policy, runbook, guide, api, faq], deny: [draft] }
update:
- { document: runbook, patch: { body: "edited" }, expect: allow }
- { document: runbook, patch: { visibility: public }, expect: deny }
- { document: runbook, patch: { status: draft }, expect: deny }
create:
- { data: { title: Section, body: "...", status: draft, visibility: team }, org: acme, team: platform, expect: allow }
- { data: { title: Section, body: "...", status: published, visibility: team }, org: acme, team: platform, expect: deny }
- { data: { title: Section, body: "...", status: draft, visibility: public }, org: acme, team: platform, expect: deny }
- name: an owner runs the company's pages and reads every published page, not other people's drafts
as: olga
read: { allow: [policy, runbook, guide, api, faq], deny: [draft] }
update:
- { document: policy, patch: { body: "edited" }, expect: allow }
- { document: runbook, patch: { body: "edited" }, expect: deny }
create:
- { data: { title: Handbook, body: "...", status: published, visibility: org }, org: acme, expect: allow }
delete: { allow: [policy], deny: [runbook] }The fixture says what each person is in orgs: roles, teams and team roles, as the server would load them for a request. A contributor whose window has closed is simply a user without that role, and the visitor is the anonymous caller every fixture has.
Around it
- Onboarding: an
on_member_joinedhook can subscribe a new member to their team’s pages. - Emails in the organization’s name: with organization context and organization templates, the “page published” email carries the organization’s brand, and each organization edits its own text.
- The app:
client.db('main').collection('pages').inTeam(team).create({...})in the JavaScript client; the team’s editors come fromclient.orgs.teams(org).members(team, { role: 'editor' }).