Skip to content

COSHH Substances

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


Purpose

COSHH Substances is the site-level register of hazardous substances in use under the Control of Substances Hazardous to Health Regulations 2002. For each substance brought onto site, the register records what it is, the hazard classification, the PPE and control measures required, storage and disposal requirements, and the emergency procedure in case of exposure.

The register serves two purposes: compliance evidence that a COSHH assessment has been carried out before the substance was used, and the reference source for COSHH briefings delivered via the Briefings module.


Entities

COSHHSubstance

One record per hazardous substance in use on a site.

Field Type Required Notes
id UUID System PK, generated
site_id UUID FK → sites.id
project_id UUID Denormalised
product_name VARCHAR(255) Trade name of the product
manufacturer VARCHAR(255) Manufacturer or supplier
hazard_classification VARCHAR(100) GHS/CLP hazard category — see values below
un_number VARCHAR(10) UN number if applicable (e.g. UN1133)
description TEXT What the substance is used for on this site
risk_level VARCHAR(20) See values below — default: medium
ppe_required JSONB List of PPE items required — see structure below
control_measures TEXT Engineering and procedural controls
storage_requirements TEXT Storage conditions, segregation, quantities
disposal_method TEXT Waste disposal procedure
emergency_procedure TEXT Actions in case of spill, exposure, or fire
assessment_file_key VARCHAR(500) S3 key for COSHH assessment document (SDS or bespoke)
assessed_by UUID FK → users.id — person who completed the assessment
assessed_date DATE
next_review_date DATE NULL = no scheduled review
status VARCHAR(20) Default: active
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_coshh_site
    ON coshh_substances(site_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_coshh_project
    ON coshh_substances(project_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_coshh_status
    ON coshh_substances(site_id, status) WHERE deleted_at IS NULL;
CREATE INDEX idx_coshh_risk
    ON coshh_substances(site_id, risk_level) WHERE deleted_at IS NULL;
-- Review scanner
CREATE INDEX idx_coshh_review
    ON coshh_substances(next_review_date) WHERE deleted_at IS NULL AND next_review_date IS NOT NULL;

Hazard classification values (GHS/CLP categories):

Value Meaning
flammable Flammable liquid, solid, gas, or aerosol
oxidising Oxidising substance or mixture
corrosive Skin corrosion or serious eye damage
toxic Acute toxicity (fatal or toxic if inhaled, swallowed, or absorbed)
harmful Harmful if swallowed, inhaled, or in contact with skin
irritant Skin or respiratory irritant
sensitiser Respiratory or skin sensitiser
carcinogen Carcinogenic, mutagenic, or reproductive toxin (CMR)
environmental Hazardous to the aquatic environment
dust_fume Hazardous dust or fume (silica, wood dust, welding fume, etc.)
other Other hazardous substance

Risk level values:

Value Meaning
low Low risk under normal use with stated controls
medium Moderate risk — controls essential
high High risk — requires specific authorisation before use

Substance status values:

Value Meaning
active Currently in use on this site
removed No longer in use — retained for record

JSONB Field Structures

ppe_required

[
  "chemical-resistant gloves",
  "safety goggles",
  "respiratory protection (FFP3 if dust generated)",
  "chemical-resistant overalls"
]

Business Rules

  1. One record per substance per site. The same product used on two different sites has two records (different site_id). Assessments may vary by site.

  2. hazard_classification is a single primary classification. If a substance has multiple hazards (e.g. flammable and toxic), select the highest-risk classification as primary and record the others in description or control_measures.

  3. assessment_file_key is optional. Many small-company assessments are recorded directly in the structured fields rather than by uploading a separate document. Both approaches are valid.

  4. Transitioning status to removed does not delete the record. It remains visible in the register for the project's retention period.

  5. A COSHH briefing for a substance is recorded in the Briefings module with category = coshh_briefing. The Briefings module does not hold a FK to coshh_substances in V1 — the link is by convention (the briefing title or content points reference the substance name).

  6. next_review_date triggers an alert when the date is within the tenant's configured advance notice window (same scanner pattern as Documents).


API Endpoints

Method Path Description
GET /sites/{siteId}/coshh List substances for this site (paginated, filterable)
POST /sites/{siteId}/coshh Register a substance
GET /sites/{siteId}/coshh/{id} Full detail
PATCH /sites/{siteId}/coshh/{id} Update substance
DELETE /sites/{siteId}/coshh/{id} Soft delete
POST /sites/{siteId}/coshh/{id}/upload-assessment Upload COSHH assessment document (presigned PUT URL)
GET /projects/{projectId}/coshh Substances across all sites

Request / Response Shapes

POST /sites/{siteId}/coshh

// Request
{
  "product_name": "Sika Primer 3N",
  "manufacturer": "Sika Limited",
  "hazard_classification": "flammable",
  "un_number": "UN1133",
  "description": "Bonding primer applied to concrete surfaces before waterproof membrane",
  "risk_level": "medium",
  "ppe_required": [
    "chemical-resistant gloves",
    "safety goggles",
    "adequate ventilation — avoid confined spaces"
  ],
  "control_measures": "Apply in well-ventilated areas only. No ignition sources within 3m. Maximum container size on site: 5L. Store in locked COSHH cabinet.",
  "storage_requirements": "Locked COSHH cabinet, away from heat sources. Max 50L on site at any time.",
  "disposal_method": "Dispose as hazardous waste. Contact waste carrier — do not pour down drain.",
  "emergency_procedure": "Eye contact: flush with water for 15 minutes, seek medical attention. Skin: wash with soap and water. Inhalation: move to fresh air. Spill: contain with sand/earth, bag and label for hazardous waste collection.",
  "assessed_by": "uuid-user",
  "assessed_date": "2026-05-28",
  "next_review_date": "2027-05-28",
  "custom_fields": {}
}

// Response 201
{
  "id": "uuid-substance",
  "site_id": "uuid-site",
  "project_id": "uuid-project",
  "product_name": "Sika Primer 3N",
  "manufacturer": "Sika Limited",
  "hazard_classification": "flammable",
  "un_number": "UN1133",
  "risk_level": "medium",
  "ppe_required": ["chemical-resistant gloves", "safety goggles", "adequate ventilation — avoid confined spaces"],
  "status": "active",
  "assessed_by": "uuid-user",
  "assessed_by_name": "James Fletcher",
  "assessed_date": "2026-05-28",
  "next_review_date": "2027-05-28",
  "assessment_file_key": null,
  "custom_fields": {},
  "created_at": "2026-05-28T10:00:00Z"
}

GET /sites/{siteId}/coshh

Query params: hazard_classification, risk_level, status (active|removed), q (product name / manufacturer search), sort (default product_name), limit, cursor


Permission Matrix

Action Platform Admin Tenant Admin Project Manager Site Manager Viewer
List / view substances ✓ assigned ✓ own site ✓ assigned
Register substance ✓ assigned ✓ own site
Edit substance ✓ assigned ✓ own site
Upload assessment document ✓ assigned ✓ own site
Mark as removed ✓ assigned ✓ own site
Delete substance ✓ assigned

Notifications Triggered

Event Trigger point Notification type
Assessment due for review Daily scanner (EventBridge 06:00 UTC) coshh.review_due

Scanner query:

SELECT *
FROM coshh_substances
WHERE deleted_at IS NULL
  AND next_review_date IS NOT NULL
  AND next_review_date BETWEEN CURRENT_DATE AND CURRENT_DATE + :advance_days
  AND status = 'active'

Offline Sync Scope

Data Syncs to Notes
COSHH register Site Manager Active substances for assigned sites
Assessment documents Not synced Fetched on demand via presigned GET URL

The register is read on site to support briefings and emergency response — offline access to the substance list (PPE, emergency procedure) is therefore important. Documents are large and fetched on demand only.


Cross-Module Dependencies

Upstream

Module Dependency
Projects & Sites site_id, project_id

Downstream

Module Dependency
Briefings COSHH briefing events reference substances by convention (no FK in V1)

Tests Required

Substances

  • Register substance — valid payload → 201
  • List substances — filter by hazard_classification, risk_level, status
  • q search matches product name and manufacturer
  • Update substance — PATCH semantics
  • Mark as removed — excluded from active list by default
  • Soft delete — excluded from list
  • Upload assessment — returns presigned PUT URL
  • Review scanner — substance with next_review_date in advance window → notification

Cross-tenant isolation (mandatory)

  • GET substance from another tenant's site → 404
  • POST substance with site_id from another tenant → 404

Permission boundaries

  • Site Manager can register and edit substances → 201
  • Site Manager cannot delete substance → 403
  • Viewer can view register → 200; cannot register → 403

Default Tenant Labels

Key Default
module.coshh COSHH Register
field.coshh.product_name Product Name
field.coshh.manufacturer Manufacturer
field.coshh.hazard_classification Hazard Classification
field.coshh.risk_level Risk Level
field.coshh.ppe_required PPE Required
field.coshh.control_measures Control Measures
field.coshh.emergency_procedure Emergency Procedure
field.coshh.assessed_date Assessment Date
field.coshh.next_review_date Next Review Date
status.coshh.active Active
status.coshh.removed Removed
risk.coshh.low Low
risk.coshh.medium Medium
risk.coshh.high High

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 / presigned_get_url core/storage.py Assessment document upload and download
audit_entity_change trigger DB foundation Applied to coshh_substances
factories.py tests/factories.py create_coshh_substance