Data Model

Complete entity-relationship model for Certexi — 15 core tables, 25 indexes, immutable event ledger, and computed state derivation patterns.

Last updated: 2025-02-18

Data Model

Certexi uses PostgreSQL with Drizzle ORM for type-safe database access. The schema follows event-sourcing principles — mutable state is always derived from an immutable event ledger.

Entity-Relationship Overview

Loading diagram…

Core Tables

operators

The authenticated users who interact with the system.

ColumnTypeDescription
operator_idtext (PK)Unique identifier
nametextDisplay name
emailtextEmail address
roleenumoperator, supervisor, admin
nextcloud_idtextLinked Nextcloud account
created_attimestampAccount creation time
last_activetimestampLast activity timestamp

events (Immutable Ledger)

🚨

Append-Only

The events table is the immutable ledger. Rows are NEVER updated or deleted. All state is derived from replaying events in chronological order.

ColumnTypeDescription
idserial (PK)Auto-increment ID
event_typeenumPLACED, REMOVED, VERIFIED, DISPUTED
slot_idint (FK)Target slot
asset_barcodetextAsset identifier
operatortext (FK)Who performed the action
timestamptimestamptzWhen it happened
photo_urltextEvidence photo URL
scale_weight_kgfloatWeight at time of event
evidence_hashtextSHA-256 hash of event bundle
merkle_roottextDaily Merkle tree root
cctv_clip_urltextOptional linked CCTV clip
work_order_reftextOptional work order reference

workflow_stage_history

Tracks every stage transition for transport units.

ColumnTypeDescription
idserial (PK)Auto-increment ID
transport_unit_idint (FK)Transport unit reference
from_stagetextPrevious stage
to_stagetextNew stage
operatortext (FK)Who triggered the transition
evidencejsonEvidence bundle
hashtextSHA-256 hash of transition
created_attimestamptzTransition time

Indexes

The schema includes 25 indexes optimized for common query patterns:

TableIndexColumnsPurpose
eventsidx_events_slot_timeslot_id, timestampSlot history queries
eventsidx_events_assetasset_barcodeAsset location lookup
eventsidx_events_operatoroperator, timestampOperator activity
eventsidx_events_type_timeevent_type, timestampType-filtered queries
eventsidx_events_time_geotimestamp, locationMap time-window queries
slotsidx_slots_zonezoneZone slot listing
transport_unitsidx_tu_stagecurrent_stageKanban queries
transport_unitsidx_tu_operatorassigned_operatorOperator workload
workflow_historyidx_wh_tutransport_unit_idUnit history

State Derivation

Current state is never stored directly — it's computed from the event ledger:

Loading diagram…

Slot Occupancy

-- Get current occupancy for a slot
SELECT DISTINCT ON (slot_id)
  slot_id, event_type, asset_barcode, timestamp
FROM events
WHERE slot_id = $1
ORDER BY slot_id, timestamp DESC;
-- If event_type = 'PLACED' → occupied
-- If event_type = 'REMOVED' → empty

Asset Location

-- Find where an asset is currently located
SELECT DISTINCT ON (asset_barcode)
  slot_id, event_type, timestamp
FROM events
WHERE asset_barcode = $1
ORDER BY asset_barcode, timestamp DESC;

Zone Utilization

-- Compute utilization per zone
WITH latest_events AS (
  SELECT DISTINCT ON (slot_id)
    s.zone, e.event_type
  FROM events e
  JOIN slots s ON s.id = e.slot_id
  ORDER BY slot_id, e.timestamp DESC
)
SELECT zone,
  COUNT(*) FILTER (WHERE event_type = 'PLACED') AS occupied,
  COUNT(*) AS total
FROM latest_events
GROUP BY zone;

Migrations

Drizzle ORM manages schema migrations with version control:

# Generate migration from schema changes
pnpm drizzle-kit generate:pg

# Apply pending migrations
pnpm drizzle-kit push:pg

# View migration status
pnpm drizzle-kit status