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¶
-
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. -
hazard_classificationis 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 indescriptionorcontrol_measures. -
assessment_file_keyis optional. Many small-company assessments are recorded directly in the structured fields rather than by uploading a separate document. Both approaches are valid. -
Transitioning
statustoremoveddoes not delete the record. It remains visible in the register for the project's retention period. -
A COSHH briefing for a substance is recorded in the Briefings module with
category = coshh_briefing. The Briefings module does not hold a FK tocoshh_substancesin V1 — the link is by convention (the briefing title or content points reference the substance name). -
next_review_datetriggers 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 qsearch 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_datein advance window → notification
Cross-tenant isolation (mandatory)¶
- GET substance from another tenant's site → 404
- POST substance with
site_idfrom 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 |