Domain model
The API exposes five core aggregates — Space, Unit, Resource, Action, Certificate — plus three role aggregates (UnitRoleAssignment, UnitRoleInvitation, SpaceAdminAssignment) covered in the Roles section. All resources are scoped to a Space: every request targets one via the X-Space-Id header, and no data ever crosses that boundary.
Tenant boundary and root of data isolation. Every other resource belongs to exactly one Space.
| Field | Type | Notes |
|---|---|---|
id | UUID | |
name | string | |
status | SpaceStatus | active or deleted. Subscription gating is independent — see Permissions → Subscription gating |
ownerUserId | string | IdP user identifier of the Space owner |
The Space lifecycle (creation, deletion) is managed by the platform, not by regular users. Access to a Space depends on the user’s membership and the subscription status.
A tag or logical grouping inside a Space — for example an office, a team, or a site. Units have no hierarchy.
| Field | Type |
|---|---|
id | UUID |
name | string |
Relations:
- many-to-many with
Resource(a Unit groups Resources) - many-to-many with
Action(a Unit requires a set of Actions)
Resource
Section titled “Resource”A person, piece of equipment, environment, or substance tracked inside a Space.
| Field | Type | Notes |
|---|---|---|
id | UUID | |
name | string | |
type | ResourceType | person, equipment, environment, or substance. Immutable after creation |
Relations:
- belongs to one Space
- tagged with zero or more Units
- has zero or more Certificates
A Resource can exist without being assigned to any Unit — in that case it carries no compliance obligations.
Action
Section titled “Action”A compliance requirement defined at Space level — for example a training course, a medical exam, or an equipment check.
| Field | Type | Notes |
|---|---|---|
id | UUID | |
name | string | |
type | ActionType | training, health, check, ppe, or maintenance |
targetResourceType | ResourceType | The Resource type this Action applies to. Immutable after creation |
validityPeriod | { value: integer, unit: "year" | "month" | "day" } | How long a Certificate for this Action stays valid |
Relations:
- belongs to one Space
- associated with zero or more Units
- has zero or more Certificates
An Action applies to Resources in the Units it is associated with, but only
when resource.type === action.targetResourceType (see Type matching).
Certificate
Section titled “Certificate”Proof of fulfillment linking a Resource to an Action.
| Field | Type |
|---|---|
id | UUID |
resourceId | UUID |
actionId | UUID |
name | string |
issuedAt | ISO 8601 timestamp |
expiresAt | ISO 8601 timestamp |
attachments | array of Attachment |
Invariants:
resourceandactionmust belong to the same Spaceaction.targetResourceType === resource.typeissuedAt <= expiresAt- An expired Certificate no longer counts as compliance coverage
Attachment
Section titled “Attachment”Attachments are embedded in the Certificate. The URL is opaque — it can point to any external store (Google Drive, Dropbox, SharePoint, etc.).
| Field | Type |
|---|---|
id | UUID |
url | string, non-empty |
label | string, non-empty |
createdAt | ISO 8601 timestamp |
Removing a Certificate cascades to its attachments.
Compliance
Section titled “Compliance”Compliance is computed on the fly — never persisted. For every (Resource, Action) pair, the API derives one of:
compliant— there is a valid Certificate withexpiresAtbeyond the configured thresholdexpiring— there is a valid Certificate but it will expire within the threshold (default 30 days)non_compliant— there is no valid Certificate for an existing obligation
A (Resource, Action) pair is an obligation when:
- the Resource is assigned to at least one Unit, and
- the Action is associated with the same Unit, and
resource.type === action.targetResourceType.
Pairs that do not match on type simply do not produce obligations — there is
no not_applicable state.
Roles (membership)
Section titled “Roles (membership)”Access control is driven by role assignments on Units and on the Space itself. See Permissions & roles for the full matrix.
| Aggregate | Purpose |
|---|---|
UnitRoleAssignment | (userId, unitId) with a UnitRole — one role per user per Unit |
UnitRoleInvitation | Placeholder for role assignments to users who have not signed up yet ((unitId, email) unique) |
SpaceAdminAssignment | Grants the Admin role at Space level. Only the Owner can add or remove Admins |
Core invariants
Section titled “Core invariants”Space isolation
Section titled “Space isolation”Every entity belongs to exactly one Space. No operation crosses Space boundaries. The Space is the boundary for security, data isolation, and billing.
Same-Space constraint
Section titled “Same-Space constraint”All relations between entities must stay within the same Space:
Resource.units[*]must belong to the same Space as the ResourceAction.units[*]must belong to the same Space as the Action- A Certificate’s Resource and Action must share the same Space
Type matching
Section titled “Type matching”An Action only applies to Resources whose type equals the Action’s
targetResourceType. This constraint is enforced both when computing
compliance and when issuing a Certificate — the API rejects mismatched pairs
with 422 Unprocessable Entity (TypeMismatchException).
No system catalog
Section titled “No system catalog”Eriga does not ship a predefined catalog of trainings, medical exams, or checks. Every Space defines its own Actions.
Entity-relationship overview
Section titled “Entity-relationship overview”erDiagram SPACE ||--o{ UNIT : contains SPACE ||--o{ RESOURCE : contains SPACE ||--o{ ACTION : contains
RESOURCE }o--o{ UNIT : "tagged with" ACTION }o--o{ UNIT : "applies to"
RESOURCE ||--o{ CERTIFICATE : has ACTION ||--o{ CERTIFICATE : has
SPACE { UUID id PK string name SpaceStatus status }
UNIT { UUID id PK string name }
RESOURCE { UUID id PK string name ResourceType type }
ACTION { UUID id PK string name ActionType type ResourceType targetResourceType ValidityPeriod validityPeriod }
CERTIFICATE { UUID id PK UUID resourceId FK UUID actionId FK timestamp issuedAt timestamp expiresAt jsonb attachments }