diff --git a/ZB.MOM.WW.Audit/src/ZB.MOM.WW.Audit/AuditEvent.cs b/ZB.MOM.WW.Audit/src/ZB.MOM.WW.Audit/AuditEvent.cs
new file mode 100644
index 0000000..5de8e2d
--- /dev/null
+++ b/ZB.MOM.WW.Audit/src/ZB.MOM.WW.Audit/AuditEvent.cs
@@ -0,0 +1,50 @@
+namespace ZB.MOM.WW.Audit;
+
+///
+/// Canonical, transport-agnostic audit record — who did what, when, with what outcome.
+/// Required core + optional common fields + a extension bag. Each
+/// sister app maps its own record onto this; domain vocabularies (channels/kinds/event-types)
+/// map into // and are not
+/// modelled here. See scadaproj/components/audit/spec/EVENT-MODEL.md.
+///
+public sealed record AuditEvent
+{
+ /// Idempotency key uniquely identifying this audit event.
+ public required Guid EventId { get; init; }
+
+ /// When the audited action occurred. Normalized to UTC on assignment.
+ /// Participates in record value-equality as a normalized instant: two events whose
+ /// OccurredAtUtc denote the same instant at different offsets (e.g. 12:00+05:00 and
+ /// 07:00Z) compare equal and share a hash code. Relevant to consumers that dedup/key on
+ /// value-equality.
+ public required DateTimeOffset OccurredAtUtc
+ {
+ get => _occurredAtUtc;
+ init => _occurredAtUtc = value.ToUniversalTime();
+ }
+ private readonly DateTimeOffset _occurredAtUtc;
+
+ /// Who performed the action (identity string; the ZB.MOM.WW.Auth principal at adoption).
+ public required string Actor { get; init; }
+
+ /// What was done — a verb/event-type string.
+ public required string Action { get; init; }
+
+ /// Normalized outcome.
+ public required AuditOutcome Outcome { get; init; }
+
+ /// Optional subsystem/grouping for the action.
+ public string? Category { get; init; }
+
+ /// Optional target of the action (resource/method/connection).
+ public string? Target { get; init; }
+
+ /// Optional node that emitted the event.
+ public string? SourceNode { get; init; }
+
+ /// Optional correlation id joining this row to its originating request/workflow.
+ public Guid? CorrelationId { get; init; }
+
+ /// Optional JSON extension carrying project-specific fields.
+ public string? DetailsJson { get; init; }
+}
diff --git a/ZB.MOM.WW.Audit/src/ZB.MOM.WW.Audit/AuditOutcome.cs b/ZB.MOM.WW.Audit/src/ZB.MOM.WW.Audit/AuditOutcome.cs
new file mode 100644
index 0000000..536561e
--- /dev/null
+++ b/ZB.MOM.WW.Audit/src/ZB.MOM.WW.Audit/AuditOutcome.cs
@@ -0,0 +1,12 @@
+namespace ZB.MOM.WW.Audit;
+
+/// Normalized outcome of an audited action.
+public enum AuditOutcome
+{
+ /// The action completed successfully.
+ Success,
+ /// The action failed due to an error.
+ Failure,
+ /// The action was rejected by authentication/authorization.
+ Denied,
+}
diff --git a/ZB.MOM.WW.Audit/src/ZB.MOM.WW.Audit/IAuditRedactor.cs b/ZB.MOM.WW.Audit/src/ZB.MOM.WW.Audit/IAuditRedactor.cs
new file mode 100644
index 0000000..f8f5f2c
--- /dev/null
+++ b/ZB.MOM.WW.Audit/src/ZB.MOM.WW.Audit/IAuditRedactor.cs
@@ -0,0 +1,13 @@
+namespace ZB.MOM.WW.Audit;
+
+///
+/// Filters an between construction and persistence — truncates oversized
+/// fields and scrubs sensitive content. Pure function: returns a filtered COPY and MUST NOT throw
+/// (over-redact on internal failure). Shaped to mirror Telemetry's ILogRedactor so a future
+/// ZB.MOM.WW.Hosting aggregator can wire both consistently; intentionally has no dependency on it.
+///
+public interface IAuditRedactor
+{
+ /// Apply the configured truncation/redaction policy and return a filtered copy.
+ AuditEvent Apply(AuditEvent rawEvent);
+}
diff --git a/ZB.MOM.WW.Audit/src/ZB.MOM.WW.Audit/IAuditWriter.cs b/ZB.MOM.WW.Audit/src/ZB.MOM.WW.Audit/IAuditWriter.cs
new file mode 100644
index 0000000..a3e6619
--- /dev/null
+++ b/ZB.MOM.WW.Audit/src/ZB.MOM.WW.Audit/IAuditWriter.cs
@@ -0,0 +1,12 @@
+namespace ZB.MOM.WW.Audit;
+
+///
+/// Best-effort sink for s. Implementations MUST swallow/log internal
+/// failures rather than propagating them — a failed audit write must never abort the
+/// user-facing action that produced it.
+///
+public interface IAuditWriter
+{
+ /// Persist an audit event. Best-effort; must not throw to the caller.
+ Task WriteAsync(AuditEvent evt, CancellationToken ct = default);
+}
diff --git a/ZB.MOM.WW.Audit/tests/ZB.MOM.WW.Audit.Tests/AuditEventTests.cs b/ZB.MOM.WW.Audit/tests/ZB.MOM.WW.Audit.Tests/AuditEventTests.cs
new file mode 100644
index 0000000..f5e17f8
--- /dev/null
+++ b/ZB.MOM.WW.Audit/tests/ZB.MOM.WW.Audit.Tests/AuditEventTests.cs
@@ -0,0 +1,67 @@
+namespace ZB.MOM.WW.Audit.Tests;
+
+public class AuditEventTests
+{
+ private static AuditEvent Minimal() => new()
+ {
+ EventId = Guid.NewGuid(),
+ OccurredAtUtc = DateTimeOffset.UtcNow,
+ Actor = "alice",
+ Action = "ConfigPublished",
+ Outcome = AuditOutcome.Success,
+ };
+
+ [Fact]
+ public void Required_core_fields_round_trip()
+ {
+ var id = Guid.NewGuid();
+ var evt = Minimal() with { EventId = id, Actor = "svc", Action = "ApiCall", Outcome = AuditOutcome.Denied };
+ Assert.Equal(id, evt.EventId);
+ Assert.Equal("svc", evt.Actor);
+ Assert.Equal("ApiCall", evt.Action);
+ Assert.Equal(AuditOutcome.Denied, evt.Outcome);
+ }
+
+ [Fact]
+ public void OccurredAtUtc_is_normalized_to_utc()
+ {
+ var local = new DateTimeOffset(2026, 6, 1, 12, 0, 0, TimeSpan.FromHours(5));
+ var evt = Minimal() with { OccurredAtUtc = local };
+ Assert.Equal(TimeSpan.Zero, evt.OccurredAtUtc.Offset);
+ Assert.Equal(local.UtcDateTime, evt.OccurredAtUtc.UtcDateTime);
+ }
+
+ [Fact]
+ public void Optional_fields_default_to_null()
+ {
+ var evt = Minimal();
+ Assert.Null(evt.Category);
+ Assert.Null(evt.Target);
+ Assert.Null(evt.SourceNode);
+ Assert.Null(evt.CorrelationId);
+ Assert.Null(evt.DetailsJson);
+ }
+
+ [Fact]
+ public void Records_with_same_values_are_equal()
+ {
+ var id = Guid.NewGuid();
+ var when = DateTimeOffset.UtcNow;
+ AuditEvent Make() => new() { EventId = id, OccurredAtUtc = when, Actor = "a", Action = "x", Outcome = AuditOutcome.Success };
+ Assert.Equal(Make(), Make());
+ }
+
+ [Fact]
+ public void Same_instant_at_different_offset_compares_equal()
+ {
+ // Guards the UTC-normalizing init-setter: if OccurredAtUtc is ever "simplified" back to a
+ // plain auto-property, these two (same instant, different offset) would stop comparing equal.
+ var id = Guid.NewGuid();
+ var utc = new DateTimeOffset(2026, 6, 1, 7, 0, 0, TimeSpan.Zero);
+ var plus5 = new DateTimeOffset(2026, 6, 1, 12, 0, 0, TimeSpan.FromHours(5)); // same instant as utc
+ AuditEvent With(DateTimeOffset when) =>
+ new() { EventId = id, OccurredAtUtc = when, Actor = "a", Action = "x", Outcome = AuditOutcome.Success };
+ Assert.Equal(With(utc), With(plus5));
+ Assert.Equal(With(utc).GetHashCode(), With(plus5).GetHashCode());
+ }
+}