Skip to content

Appointed Roles

Status: Specified — ready to build Build order: 12 of 13 — depends on Projects & Sites, Personnel


Purpose

Appointed Roles is the project-level register of formal safety role appointments. UK H&S legislation and construction contract requirements mandate that specific roles are held by named, qualified individuals on every project and site — Fire Marshal, First Aider, COSHH Coordinator, Lifting Appointed Person, and others. This module tracks who holds each appointment, their qualification status, and the formal appointment letter on record.

The register surfaces gaps: roles with no appointee, nominations awaiting approval, and roles where the appointed person's qualification is expiring.


Entities

RoleDefinition

A standard safety role that can be appointed on a project. Platform provides a default set; tenants can add custom roles.

Field Type Required Notes
id UUID System PK, generated
project_id UUID FK → projects.id — NULL = platform standard role
name VARCHAR(255) Role title
description TEXT Competence requirements and responsibilities
requires_qualification BOOLEAN Default: true — whether linked licence/card is expected
custom_fields JSONB Default {}
created_at TIMESTAMPTZ System
updated_at TIMESTAMPTZ System Trigger-maintained
created_by UUID System FK → users.id
updated_by UUID System
deleted_at TIMESTAMPTZ
CREATE INDEX idx_role_definition_project
    ON role_definitions(project_id) WHERE deleted_at IS NULL;

Platform standard roles:

Role Notes
First Aider Valid First Aid at Work certificate required
Fire Marshal Fire extinguisher trained; adequate authority
Fire Safety Coordinator Senior fire safety role
COSHH Coordinator CITB SMSTS or IOSH Managing Safely + COSHH Coordinator training
Lifting Appointed Person (AP) Experienced, valid CPCS A61 or equivalent
Lifting Operations Supervisor CPCS or CITB qualified
Lifting Operations Slinger/Signaller CPCS A40 or equivalent
PVPC — People, Vehicle & Plant Coordinator
Utility Coordinator EUSR Service Avoidance or equivalent
AP — Permit to Break Ground EUSR Category 1 & 2 or equivalent
Electrical SSoW Appointed Person
MEWP Coordinator
Confined Space Coordinator
Environmental Coordinator
Waste Coordinator
Mental Health First Aider
Hot Works Responsible Person
Manual Handling Competent Person
Temporary Works Coordinator
Responsible Person (Excavation Gang) EUSR Category 1 & 2

RoleAppointment

The record of a person holding (or being nominated for) a specific role on a project or site.

Field Type Required Notes
id UUID System PK, generated
project_id UUID FK → projects.id
site_id UUID FK → sites.id — NULL = project-wide appointment
role_definition_id UUID FK → role_definitions.id
personnel_id UUID FK → personnel.id
status VARCHAR(30) See values below — default: nominated
effective_date DATE Date the appointment takes effect
expiry_date DATE NULL = no expiry
appointment_letter_key VARCHAR(500) S3 key for signed appointment letter
notes TEXT Any conditions or limitations on the appointment
custom_fields JSONB Default {}
created_at TIMESTAMPTZ System
updated_at TIMESTAMPTZ System Trigger-maintained
created_by UUID System FK → users.id
updated_by UUID System
deleted_at TIMESTAMPTZ
CREATE INDEX idx_role_appointment_project
    ON role_appointments(project_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_role_appointment_site
    ON role_appointments(site_id) WHERE deleted_at IS NULL AND site_id IS NOT NULL;
CREATE INDEX idx_role_appointment_personnel
    ON role_appointments(personnel_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_role_appointment_status
    ON role_appointments(project_id, status) WHERE deleted_at IS NULL;
-- Expiry scanner
CREATE INDEX idx_role_appointment_expiry
    ON role_appointments(expiry_date) WHERE deleted_at IS NULL AND expiry_date IS NOT NULL;

Appointment status values:

Status Meaning
appointed Formally appointed — appointment letter on file
nominated Person identified; approval or paperwork outstanding
training_required Person identified but does not yet hold the required qualification

Business Rules

  1. Multiple people can hold the same role on a project. There is no uniqueness constraint on (project_id, role_definition_id) — a project may have three First Aiders.

  2. A site_id scopes the appointment to a specific site. A project-wide appointment (site_id = NULL) covers all sites on the project. Both can coexist — a project-wide Lifting AP and a site-specific Lifting AP are separate records.

  3. Transitioning status to appointed does not require an appointment_letter_key — for smaller projects, verbal confirmation may be recorded in notes. The letter upload is encouraged but not enforced by the API.

  4. The list endpoint surfaces a "gap" view: for each standard role, it shows whether the project has at least one appointed holder. Roles with no appointed holder are flagged in the response.

  5. Platform standard role definitions (project_id = NULL) cannot be edited or deleted. Tenants can create additional project-level roles.

  6. expiry_date on an appointment reflects when the appointment itself expires (not the person's underlying qualification). The expiry scanner generates a notification when an appointment is approaching expiry.


API Endpoints

Role Definitions

Method Path Description
GET /projects/{projectId}/role-definitions List standard + project-specific roles
POST /projects/{projectId}/role-definitions Create project-specific role
PATCH /projects/{projectId}/role-definitions/{id} Update project-specific role
DELETE /projects/{projectId}/role-definitions/{id} Soft delete project-specific role

Appointments

Method Path Description
GET /projects/{projectId}/appointed-roles Full appointment register with gap view
POST /projects/{projectId}/appointed-roles Create appointment
GET /projects/{projectId}/appointed-roles/{id} Appointment detail
PATCH /projects/{projectId}/appointed-roles/{id} Update appointment
DELETE /projects/{projectId}/appointed-roles/{id} Soft delete
POST /projects/{projectId}/appointed-roles/{id}/upload-letter Upload appointment letter (returns presigned PUT URL)

Request / Response Shapes

POST /projects/{projectId}/appointed-roles

// Request
{
  "site_id": null,
  "role_definition_id": "uuid-role-def",
  "personnel_id": "uuid-person",
  "status": "appointed",
  "effective_date": "2026-05-28",
  "expiry_date": null,
  "notes": "CPCS A61 card verified — copy on file."
}

// Response 201
{
  "id": "uuid-appointment",
  "project_id": "uuid-project",
  "site_id": null,
  "role": {
    "id": "uuid-role-def",
    "name": "Lifting Appointed Person (AP)",
    "requires_qualification": true
  },
  "personnel": {
    "id": "uuid-person",
    "full_name": "James Fletcher",
    "job_title": "Crane Supervisor"
  },
  "status": "appointed",
  "effective_date": "2026-05-28",
  "expiry_date": null,
  "appointment_letter_key": null,
  "notes": "CPCS A61 card verified — copy on file.",
  "created_at": "2026-05-28T09:00:00Z"
}

GET /projects/{projectId}/appointed-roles

Query params: site_id (NULL for project-wide, UUID for site-specific, all for both), status, role_definition_id, personnel_id, show_gaps (default true), limit, cursor

// Response 200
{
  "appointments": [
    {
      "id": "uuid-appointment",
      "role": { "id": "uuid-role-def", "name": "First Aider" },
      "personnel": { "id": "uuid-person", "full_name": "Sarah Ahmed" },
      "status": "appointed",
      "site_id": null,
      "effective_date": "2026-05-01",
      "expiry_date": null
    }
  ],
  "gaps": [
    {
      "role_definition_id": "uuid-role-def-2",
      "role_name": "Fire Marshal",
      "has_appointed": false,
      "nominations": 1
    }
  ],
  "pagination": { "total": 8, "cursor": null, "has_more": false }
}

Permission Matrix

Action Platform Admin Tenant Admin Project Manager Site Manager Viewer
View appointment register ✓ assigned ✓ own site ✓ assigned
Create / edit appointment ✓ assigned
Transition status ✓ assigned
Upload appointment letter ✓ assigned
Delete appointment ✓ assigned
Create project-specific role definition ✓ assigned

Notifications Triggered

Event Trigger point Notification type
Appointment approaching expiry Daily scanner (EventBridge 06:00 UTC) appointed_role.expiry

Scanner query:

SELECT *
FROM role_appointments
WHERE deleted_at IS NULL
  AND expiry_date IS NOT NULL
  AND expiry_date BETWEEN CURRENT_DATE AND CURRENT_DATE + :advance_days
  AND status = 'appointed'

Offline Sync Scope

Data Syncs to Notes
RoleDefinitions Project Manager, Site Manager Standard + project roles
RoleAppointments Project Manager, Site Manager Full register for assigned project

Offline reads are sufficient — appointments are not created or edited on site in normal workflow.


Cross-Module Dependencies

Upstream

Module Dependency
Projects & Sites project_id, optional site_id
Personnel personnel_id

Downstream

None. Appointed Roles is a terminal module.


Tests Required

Role Definitions

  • List includes platform standard roles + project-specific roles
  • Create project-specific role → 201
  • Cannot delete platform standard role → 403

Appointments

  • Create appointment — valid → 201
  • Create appointment — multiple people in same role → 201 (allowed)
  • Create project-wide appointment (site_id = null) → 201
  • Create site-scoped appointment → 201
  • Transition status nominatedappointed → 200
  • Upload appointment letter — returns presigned PUT URL
  • List with show_gaps=true — gaps array populated for roles with no appointed holder
  • Expiry scanner — appointment with expiry_date in advance window → notification

Cross-tenant isolation (mandatory)

  • GET appointments from another tenant's project → 404
  • POST appointment with personnel_id from another tenant → 404

Permission boundaries

  • Site Manager cannot create or edit appointment → 403
  • Project Manager can create appointment → 201
  • Viewer can view register → 200

Default Tenant Labels

Key Default
module.appointed_roles Appointed Roles
field.appointment.role Role
field.appointment.status Appointment Status
field.appointment.effective_date Effective Date
field.appointment.expiry_date Expiry Date
field.appointment.appointment_letter Appointment Letter
status.appointment.appointed Appointed
status.appointment.nominated Nominated
status.appointment.training_required Training Required

Foundation Dependencies

Component Location Used for
TenantMiddleware core/middleware.py Sets request.state.tenant
get_db core/db.py Schema-scoped AsyncSession
get_current_user core/security.py Auth
require_permission core/security.py RBAC
EntityMixin core/models.py Universal columns
PaginatedResponse core/pagination.py List endpoint wrapper
ConstruoError hierarchy core/exceptions.py Typed exceptions
presigned_put_url core/storage.py Appointment letter upload
audit_entity_change trigger DB foundation Applied to role_appointments
factories.py tests/factories.py create_role_appointment