IoT Timeline Bridge
Granit.IoT.Timeline is a 15-line handler module that listens to device
domain events and writes them as TimelineEntryType.SystemLog entries.
Your support team, your compliance auditor, and your mobile app all see
the same audit trail — no extra code, no second schema.
What problem does this bridge solve?
Section titled “What problem does this bridge solve?”ISO 27001 asset traceability and GDPR audit logs both demand who did what, when, and why on every long-lived asset. Without a bridge, IoT events land in three different places:
- In the database audit columns.
CreatedBy/ModifiedBytell you who last touched the row — not the full sequence of transitions. - In structured logs. Searchable, yes. But not joined to the device record in the admin UI, and rotated away after 30 days.
- In nowhere at all.
Suspend("sensor was decommissioned by accident")without a persistent audit entry means the next incident reviewer reinvents the history.
The Timeline bridge consolidates all three into the same activity-stream your other entities (invoices, parties, documents) already use.
The five bridged events
Section titled “The five bridged events”flowchart LR
E1["DeviceProvisionedEvent"] --> T["DeviceTimelineHandler"]
E2["DeviceActivatedEvent"] --> T
E3["DeviceSuspendedEvent"] --> T
E4["DeviceReactivatedEvent"] --> T
E5["DeviceDecommissionedEvent"] --> T
T --> W["ITimelineWriter.PostEntryAsync"]
W --> DB[("timeline_entries<br/>entityType = 'Device'")]
DB --> UI["Admin UI<br/>device detail page"]
DB --> API["GET /timeline/Device/{id}"]
| Domain event | Timeline body |
|---|---|
DeviceProvisionedEvent | Device provisioned: {SerialNumber} (status = Provisioning) |
DeviceActivatedEvent | Device activated: {SerialNumber} (Provisioning → Active) |
DeviceSuspendedEvent | Device suspended (Active → Suspended): reason = '{Reason}' |
DeviceReactivatedEvent | Device reactivated: {SerialNumber} (Suspended → Active) |
DeviceDecommissionedEvent | Device decommissioned (status = Decommissioned) |
Each entry carries entityType = "Device" and entityId = deviceId. The
Timeline module resolves the authoring user from ICurrentUser
automatically, so the audit trail is attributable to a real principal —
not just “system”.
Registration
Section titled “Registration”Bundled in Granit.Bundle.IoT:
builder.Services.AddGranit(builder.Configuration).AddIoT();Or standalone:
builder.Services.AddGranitIoTTimeline();GranitIoTTimelineModule depends on GranitIoTModule and
GranitTimelineModule. The handlers are internal static partial —
Wolverine auto-discovers them on startup, so no manual handler
registration is needed.
Querying the timeline
Section titled “Querying the timeline”Granit.Timeline.Endpoints exposes
/api/timeline/{entityType}/{entityId} by default:
curl "https://your-app/api/timeline/Device/$DEVICE_ID" \ -H "Authorization: Bearer $TOKEN"Response includes each system-log entry in chronological order — exactly what a support engineer needs when “device goes offline” turns into a support ticket.
Why this matters for compliance
Section titled “Why this matters for compliance”| Requirement | Met by |
|---|---|
| ISO 27001 A.8.1.1 — asset inventory management | Full provenance trail per device (provision, activate, suspend, reactivate, decommission) |
| ISO 27001 A.12.4.1 — event logging | Device lifecycle events logged in a tamper-resistant timeline, durable past log rotation |
| GDPR Art. 30 — records of processing activities | Tenant-scoped timeline entries let a DPO reconstruct who touched which device |
Because Granit.Timeline entries are tenant-scoped and permission-checked,
a tenant admin can grant auditors read-only access to their own device
chatter without exposing any other tenant’s data.
Extending the bridge
Section titled “Extending the bridge”Need to surface custom device events (firmware update, credential rotation, custom workflow transition) in the same timeline?
-
Raise an
IDomainEventfrom your code. -
Add a handler method in your own
internal static partialclass:public static Task HandleAsync(FirmwareUpdatedEvent e,ITimelineWriter writer,CancellationToken ct) =>writer.PostEntryAsync(entityType: "Device",entityId: e.DeviceId.ToString(),entryType: TimelineEntryType.SystemLog,body: $"Firmware updated: {e.PreviousVersion} → {e.NewVersion}",cancellationToken: ct);
Wolverine picks it up automatically.
Anti-patterns to avoid
Section titled “Anti-patterns to avoid”See also
Section titled “See also”- Device management — the domain events that drive the handlers
- Notifications bridge — the other Ring 3 bridge
- Operations — offline detection also creates timeline entries