Skip to content

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

WhoIsCan
Olgaowner of the organizationwrite the company’s pages; read every published page
Anamember of Platform, editor therewrite, publish and decide who reads Platform’s pages
Benmember of Platformread Platform’s pages, the organization’s and the public ones
Carlamember of Support, contributor in Platform for two weeksstart drafts and edit the text of Platform’s pages
Danmember of Supportread what is shared with the organization and Support’s pages
A visitornobody, no accountread 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 period
  • editor requires a second factor: without one it only counts as a plain member.
  • editor can grant contributor, in its own team.
  • There is no role for “reads the team’s pages”: inTeam says 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: optional lets the same collection hold the company’s pages and each team’s. A team page is created with ?team=.
  • visibility decides who reads a published page: team (its members, editors and contributors), org (every member of the organization) or public (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 changing status, which only editors (and owners, for company pages) do.
  • read is 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 leave status and visibility as they are. delete lets 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_joined hook 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 from client.orgs.teams(org).members(team, { role: 'editor' }).
Edit this page · Last updated 13 Nov 2026