Example: HR absences
A company's absence module built with team roles: employees ask, department managers and delegates decide, HR sees everything. A tested realm.
An HR module for a B2B app: each company is an organization, each department a team, and people ask for absences that their manager (or someone covering) approves. The whole realm is examples/config/hr, and its rules are tested with backd rules test.
Who is who
| Who | Is | Can |
|---|---|---|
| Ana | member of Sales | ask for an absence; edit or delete it while it is requested |
| Marta | member of Sales, manager there | see and decide Sales’ absences |
| Dario | member of Support, delegate in Sales for two weeks | see and decide Sales’ absences in that period |
| Hugo | hr in the company | see every absence of the company |
The realm
examples/config/hr/realm.yaml
# The HR absence module of docs/organizations/team-access-recipes: one company per
# organization, departments as teams, managers per department, delegation with dates.
auth: enabled
signup: open
organizations:
roles:
owner: { description: Runs the company account, manage: [org, members, roles, teams] }
hr:
description: Sees every absence of the company and approves long ones
manage: [members, teams]
require_mfa: true
member: { description: Every employee }
default_roles: [member]
# Roles held within one department.
team_roles:
manager:
description: Approves the department's absences
grants: [delegate] # may hand approval to someone else, in this department only
require_mfa: true
delegate:
description: Approves in place of the manager, for a periodmanagerandhrrequire a second factor: without one they only count as plain members.managercan grantdelegate, in its own department.- There is no role for “sees the department”:
inTeamand the team roles say it.
The collection and its rules
examples/config/hr/main/absences/collection.yaml
# An absence belongs to a company and to the employee's department. What each person may do
# comes from who they are in that department, never from copying names into the document.
organization: required
on_org_delete:
action: delete
team: required
on_team_delete:
action: keep # the history stays, readable by HR through the organization
rules:
# Employees see their own; the manager and delegates of the department see its absences,
# without having to belong to it; HR sees the company's.
read: >
user != nil && (
document._meta.owner == user.id
|| hasTeamRole(document._meta.team, 'manager')
|| hasTeamRole(document._meta.team, 'delegate')
|| hasOrgRole(document._meta.org, 'hr')
)
# Anyone asks for an absence in their own department, as a request.
create: user != nil && inTeam(data._meta.team) && data.status == 'requested'
# The employee edits their own request; approving needs a manager or a delegate (in force today).
update: >
user != nil && (
(document._meta.owner == user.id && document.status == 'requested' && data.status == 'requested')
|| hasTeamRole(document._meta.team, 'manager')
|| hasTeamRole(document._meta.team, 'delegate')
)
delete: user != nil && document._meta.owner == user.id && document.status == 'requested'readis filtered inside the query: Ana’s list is her own absences; Marta’s is Sales’; Hugo’s the company’s. Nobody receives a document to hide it later.createneeds the department in the request (?org=&team=) and a team the person belongs to, and an absence is bornrequested.updatelets the employee edit their own request and nothing more, and lets a manager or delegate decide.on_team_delete: keep: when a department is deleted its absences stay, with the old team id, readable by HR.
Giving and ending cover
POST /v1/acme/_orgs/{org}/teams/{sales}/roles # as Marta
{ "user": "<dario>", "role": "delegate", "from": "2026-08-01T00:00:00Z", "until": "2026-08-15T00:00:00Z" }
From 1 August Dario’s list of absences includes Sales’; on the 15th it doesn’t, with nothing to clean up. Marta can end it earlier with DELETE on the same path.
The tests
examples/config/hr/main/absences/rules.test.yaml
# Who sees and decides what: the employee, a delegate from another department, the manager and HR.
users:
ana: { id: u1, email: ana@acme.example, orgs: { acme: { roles: [member], teams: [sales] } } }
ben: { id: u2, email: ben@acme.example, orgs: { acme: { roles: [member], teams: [sales] } } }
marta: { id: u3, email: marta@acme.example, orgs: { acme: { roles: [member], teams: [sales], team_roles: { sales: [manager] } } } }
# Covers Sales from Support: holds delegate there, without belonging to it.
dario: { id: u4, email: dario@acme.example, orgs: { acme: { roles: [member], teams: [support], team_roles: { sales: [delegate] } } } }
hugo: { id: u5, email: hugo@acme.example, orgs: { acme: { roles: [member, hr] } } }
elsa: { id: u6, email: elsa@acme.example, orgs: { acme: { roles: [member], teams: [support] } } }
documents:
leave: { kind: vacation, from: "2026-08-03", until: "2026-08-14", status: requested, _meta: { owner: u1, org: acme, team: sales } }
sick: { kind: sick, from: "2026-09-01", until: "2026-09-02", status: requested, _meta: { owner: u2, org: acme, team: sales } }
other: { kind: personal, from: "2026-09-10", until: "2026-09-10", status: requested, _meta: { owner: u6, org: acme, team: support } }
tests:
- name: an employee sees and edits only their own request
as: ana
read: { allow: [leave], deny: [sick, other] }
update:
- { document: leave, patch: { note: "Moved to August" }, expect: allow }
- { document: leave, patch: { status: approved }, expect: deny }
delete: { allow: [leave] }
create:
- { data: { kind: vacation, from: "2026-12-20", until: "2026-12-31", status: requested }, org: acme, team: sales, expect: allow }
- { data: { kind: vacation, from: "2026-12-20", until: "2026-12-31", status: approved }, org: acme, team: sales, expect: deny }
- { data: { kind: vacation, from: "2026-12-20", until: "2026-12-31", status: requested }, org: acme, team: support, expect: deny }
- name: the manager of Sales approves Sales' absences, and only theirs
as: marta
read: { allow: [leave, sick], deny: [other] }
update:
- { document: leave, patch: { status: approved }, expect: allow }
- { document: other, patch: { status: approved }, expect: deny }
- name: a delegate from another department decides for Sales without being in it
as: dario
read: { allow: [leave, sick], deny: [other] }
update:
- { document: sick, patch: { status: approved }, expect: allow }
- name: HR reads the whole company and doesn't decide for a department
as: hugo
read: { allow: [leave, sick, other] }
update:
- { document: leave, patch: { status: approved }, expect: deny }
- name: another department's employee sees nothing of Sales
as: elsa
read: { allow: [other], deny: [leave, sick] }The fixture says what each person is in orgs: roles, teams and team roles, as the server would load them for a request. A delegate whose window has closed is simply a user without that role.
Around it
- Onboarding: an
on_member_joinedhook creates the employee’s profile, subscribed to their department. - Emails in the company’s name: with company context and owner templates, the “absence requested” and “absence decided” emails carry the company’s brand, and each company edits its own text.
- The app:
client.db('main').collection('absences').inOrg(org).inTeam(team).create({...})in the JavaScript client; the department’s managers come fromclient.orgs.teams(org).members(team, { role: 'manager' }).