Skip to content

Documents

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


Purpose

Documents is the version-controlled register for all project and site documents — drawings, method statements, RAMS, permits, certificates, and any other file the project needs to track. Documents with expiry dates trigger alerts before they lapse. Each document has a controlled version history: superseded revisions are retained but clearly marked.


Entities

Document

A single controlled document in the register. The document itself is a logical record; files are attached as DocumentRevision records.

Field Type Required Notes
id UUID System PK, generated
project_id UUID FK → projects.id
site_id UUID FK → sites.id — NULL = project-level document
title VARCHAR(255) Document title
document_number VARCHAR(100) User-defined reference (e.g. drawing number)
category VARCHAR(100) See categories below
description TEXT
expiry_date DATE NULL = does not expire
current_revision_id UUID FK → document_revisions.id — latest revision
status VARCHAR(20) Default: current
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_documents_project_id
    ON documents(project_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_documents_site_id
    ON documents(site_id) WHERE deleted_at IS NULL AND site_id IS NOT NULL;
CREATE INDEX idx_documents_category
    ON documents(project_id, category) WHERE deleted_at IS NULL;
CREATE INDEX idx_documents_status
    ON documents(project_id, status) WHERE deleted_at IS NULL;
-- Expiry scanner
CREATE INDEX idx_documents_expiry
    ON documents(expiry_date) WHERE deleted_at IS NULL AND expiry_date IS NOT NULL;

Document categories:

Category Typical use
RAMS Risk Assessment and Method Statement
Drawing Engineering drawings — as-built, scheme, construction
Certificate Inspection, test, or approval certificate
Permit Permit to work, permit to dig, hot works permit
Site Instruction Formal written instruction from client/engineer
Contract Document Contract, NEC schedule, specification
Insurance Project-level insurance documents
Health & Safety H&S plan, fire risk assessment, COSHH
Environmental Environmental management plan, waste consignment
Commissioning Test records, O&M manuals
Other

Document status values:

Status Meaning
current Latest approved revision — in active use
superseded Older revision — retained for record
archived No longer in use

DocumentRevision

A specific version/revision of a document. Each upload creates a new revision. The previous revision is retained in the register as superseded.

Field Type Required Notes
id UUID System PK, generated
document_id UUID FK → documents.id
project_id UUID Denormalised
revision VARCHAR(20) e.g. Rev A, Rev B, v1.0, P01, C01
file_key VARCHAR(500) S3 object key
file_name VARCHAR(255) Original file name
file_size_bytes INTEGER
mime_type VARCHAR(100)
change_description TEXT What changed in this revision
uploaded_at TIMESTAMPTZ
uploaded_by UUID FK → users.id
created_at TIMESTAMPTZ System
deleted_at TIMESTAMPTZ
CREATE INDEX idx_doc_revision_document
    ON document_revisions(document_id, uploaded_at DESC) WHERE deleted_at IS NULL;

Business Rules

  1. Every upload creates a new DocumentRevision. The document record's current_revision_id is updated to the new revision. The previous revision is retained (not deleted).

  2. expiry_date is on the Document record, not the revision. It represents when the document itself expires (e.g. a permit expires, a certificate is reviewed annually). It does not change when a new revision is uploaded.

  3. site_id is optional. Project-level documents (drawings, contracts, RAMS) have site_id = NULL. Site-level documents (site-specific permits) have site_id set.

  4. The list endpoint returns the document record with the current revision's metadata inlined. The revision history is fetched separately.

  5. document_number is not unique — the same drawing can have multiple documents (different sites, superseded originals). It is a display identifier, not a primary key.

  6. Soft deleting a document does not delete its revisions. Files remain in S3 for the tenant's retention period (from tenant.retention_days).

  7. Uploading a new revision automatically transitions the document to current if it was archived. Restoring a document by uploading a new revision is the intended workflow.


API Endpoints

Documents

Method Path Description
GET /projects/{projectId}/documents List all project documents (paginated, filterable)
POST /projects/{projectId}/documents Create document record
GET /projects/{projectId}/documents/{id} Full detail with current revision
PATCH /projects/{projectId}/documents/{id} Update metadata (not file)
DELETE /projects/{projectId}/documents/{id} Soft delete
POST /projects/{projectId}/documents/{id}/transition Status transition

Revisions

Method Path Description
GET /projects/{projectId}/documents/{id}/revisions Full revision history
POST /projects/{projectId}/documents/{id}/revisions Upload new revision (returns presigned PUT URL)
GET /projects/{projectId}/documents/{id}/revisions/{revId}/download Presigned GET URL for file

Site-level view

Method Path Description
GET /sites/{siteId}/documents Documents scoped to this site

Request / Response Shapes

POST /projects/{projectId}/documents

// Request — create the document record first
{
  "title": "Structural Steelwork RAMS — Grid A–D",
  "document_number": "CWPH2-RAMS-001",
  "category": "RAMS",
  "site_id": null,
  "description": "Method statement for erection of structural steelwork to Grids A through D",
  "expiry_date": "2027-05-28",
  "custom_fields": {}
}

// Response 201
{
  "id": "uuid-doc",
  "project_id": "uuid-project",
  "site_id": null,
  "title": "Structural Steelwork RAMS — Grid A–D",
  "document_number": "CWPH2-RAMS-001",
  "category": "RAMS",
  "description": "...",
  "expiry_date": "2027-05-28",
  "days_until_expiry": 365,
  "expiry_status": "valid",
  "current_revision": null,
  "status": "current",
  "custom_fields": {},
  "created_at": "2026-05-28T09:00:00Z"
}

POST /projects/{projectId}/documents/{id}/revisions

// Request
{
  "revision": "Rev A",
  "file_name": "CWPH2-RAMS-001-RevA.pdf",
  "file_size_bytes": 1243648,
  "mime_type": "application/pdf",
  "change_description": "Initial issue for approval"
}

// Response 201
{
  "revision_id": "uuid-revision",
  "document_id": "uuid-doc",
  "revision": "Rev A",
  "file_name": "CWPH2-RAMS-001-RevA.pdf",
  "file_size_bytes": 1243648,
  "mime_type": "application/pdf",
  "upload_url": "https://s3.eu-west-2.amazonaws.com/construo-files-prod/...?X-Amz-...",
  "upload_expires_at": "2026-05-28T09:15:00Z"
}

The client uploads directly to S3 using the presigned URL, then calls a confirm endpoint to mark the upload as complete.

GET /projects/{projectId}/documents

Query params: category, site_id (NULL for project-level, a UUID for site-level), status, expiry_status (expired|expiring_soon|valid|no_expiry), q (title/document_number search), sort (default title), limit, cursor

// Response 200
{
  "data": [
    {
      "id": "uuid-doc",
      "title": "Structural Steelwork RAMS — Grid A–D",
      "document_number": "CWPH2-RAMS-001",
      "category": "RAMS",
      "expiry_date": "2027-05-28",
      "expiry_status": "valid",
      "days_until_expiry": 365,
      "current_revision": {
        "revision": "Rev A",
        "file_name": "CWPH2-RAMS-001-RevA.pdf",
        "uploaded_at": "2026-05-28T09:05:00Z",
        "uploaded_by_name": "James Fletcher"
      },
      "status": "current"
    }
  ],
  "pagination": { "total": 24, "cursor": null, "has_more": false }
}

Permission Matrix

Action Platform Admin Tenant Admin Project Manager Site Manager Viewer
List / view documents ✓ assigned ✓ assigned ✓ assigned
Create document ✓ assigned ✓ own site (site-level only)
Upload revision ✓ assigned ✓ own site (site-level only)
Edit metadata ✓ assigned ✓ own site
Transition status ✓ assigned
Delete document ✓ assigned
Download file ✓ assigned ✓ own site ✓ assigned
View revision history ✓ assigned ✓ own site ✓ assigned

Site Managers can upload and manage documents scoped to their own site. They cannot manage project-level documents (drawings, contracts, RAMS at project level).


Notifications Triggered

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

Scanner query:

SELECT *
FROM documents
WHERE deleted_at IS NULL
  AND expiry_date IS NOT NULL
  AND expiry_date BETWEEN CURRENT_DATE AND CURRENT_DATE + :advance_days
  AND status != 'archived'

Offline Sync Scope

Data Syncs to Notes
Document list Project Manager, Site Manager Metadata only — no file content
Current revision metadata Project Manager, Site Manager File name, date, revision
File content Not synced On-demand presigned URL download

Document files are large and variable in size — they are fetched on demand via presigned GET URLs rather than synced to local storage in V1.


Cross-Module Dependencies

Upstream

Module Dependency
Projects & Sites project_id, optional site_id

Downstream

None. Documents is a terminal module.


Tests Required

Documents

  • Create document — valid → 201, no current revision
  • List documents — filter by category, status, expiry_status
  • Expiry status computed correctly for all four states
  • q search matches title and document_number
  • Transition to archived → status updated
  • Soft delete — document excluded from list

Revisions

  • Upload revision — returns presigned PUT URL
  • Confirm upload — current_revision_id updated on document
  • Second revision uploaded — current_revision_id updated to new revision
  • Revision history lists both revisions in reverse chronological order
  • Download endpoint — returns presigned GET URL with short expiry

Expiry

  • days_until_expiry computed correctly (positive, zero, negative)
  • expiry_status correct for all states

Cross-tenant isolation (mandatory)

  • GET document from another tenant's project → 404
  • POST revision with document_id from another tenant → 404

Permission boundaries

  • Site Manager can create site-level document → 201
  • Site Manager cannot create project-level document (site_id = null) → 403
  • Site Manager cannot delete document → 403
  • Viewer can view and download → 200; cannot upload → 403

Default Tenant Labels

Key Default
module.documents Documents
field.document.document_number Document Number
field.document.category Category
field.document.expiry_date Expiry Date
field.revision.revision Revision
field.revision.change_description Change Description
status.document.current Current
status.document.superseded Superseded
status.document.archived Archived

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 File upload and download
audit_entity_change trigger DB foundation Applied to documents, document_revisions
factories.py tests/factories.py create_document, create_document_revision