diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/IAuditLogRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/IAuditLogRepository.cs
new file mode 100644
index 0000000..7b15962
--- /dev/null
+++ b/src/ScadaLink.Commons/Interfaces/Repositories/IAuditLogRepository.cs
@@ -0,0 +1,56 @@
+using ScadaLink.Commons.Entities.Audit;
+using ScadaLink.Commons.Types.Audit;
+
+namespace ScadaLink.Commons.Interfaces.Repositories;
+
+///
+/// Append-only data access for the central AuditLog table (Audit Log #23).
+///
+///
+///
+/// The append-only invariant is enforced both at the SQL level (the
+/// scadalink_audit_writer role has only INSERT + SELECT — UPDATE and DELETE
+/// are not granted) and at the API level: this interface deliberately exposes no
+/// Update and no single-row Delete. Bulk purge is performed exclusively via
+/// monthly partition switch-out ().
+///
+///
+/// Ingest is idempotent on EventId: is
+/// first-write-wins, so retrying telemetry and reconciliation pulls can both feed
+/// the same writer without producing duplicates.
+///
+///
+public interface IAuditLogRepository
+{
+ ///
+ /// Inserts if no row with the same
+ /// exists; otherwise silently leaves the
+ /// stored row untouched (first-write-wins). Bypasses the EF change tracker
+ /// so the row never enters a tracked state.
+ ///
+ Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default);
+
+ ///
+ /// Returns up to rows matching
+ /// , ordered by (OccurredAtUtc DESC, EventId DESC).
+ /// Use keyset paging by passing the last returned row's
+ /// OccurredAtUtc + EventId back via
+ /// +
+ /// to fetch the next page.
+ ///
+ Task> QueryAsync(
+ AuditLogQueryFilter filter,
+ AuditLogPaging paging,
+ CancellationToken ct = default);
+
+ ///
+ /// Switches out (purges) the monthly partition whose lower bound is
+ /// . The honest M1 implementation throws
+ /// : the UX_AuditLog_EventId unique
+ /// index is non-partition-aligned (lives on [PRIMARY], not on
+ /// ps_AuditLog_Month), so SQL Server rejects
+ /// ALTER TABLE … SWITCH PARTITION until the drop-and-rebuild dance
+ /// shipped by the M6 purge actor is in place.
+ ///
+ Task SwitchOutPartitionAsync(DateTime monthBoundary, CancellationToken ct = default);
+}
diff --git a/src/ScadaLink.Commons/Types/Audit/AuditLogPaging.cs b/src/ScadaLink.Commons/Types/Audit/AuditLogPaging.cs
new file mode 100644
index 0000000..7feeffd
--- /dev/null
+++ b/src/ScadaLink.Commons/Types/Audit/AuditLogPaging.cs
@@ -0,0 +1,13 @@
+namespace ScadaLink.Commons.Types.Audit;
+
+///
+/// Keyset paging cursor for .
+/// The repository orders by (OccurredAtUtc DESC, EventId DESC); callers pass
+/// the last row of the previous page back as +
+/// to fetch the next page. Both must be non-null together,
+/// or both null (first page).
+///
+public sealed record AuditLogPaging(
+ int PageSize,
+ DateTime? AfterOccurredAtUtc = null,
+ Guid? AfterEventId = null);
diff --git a/src/ScadaLink.Commons/Types/Audit/AuditLogQueryFilter.cs b/src/ScadaLink.Commons/Types/Audit/AuditLogQueryFilter.cs
new file mode 100644
index 0000000..4e2001a
--- /dev/null
+++ b/src/ScadaLink.Commons/Types/Audit/AuditLogQueryFilter.cs
@@ -0,0 +1,21 @@
+using ScadaLink.Commons.Types.Enums;
+
+namespace ScadaLink.Commons.Types.Audit;
+
+///
+/// Filter predicate for .
+/// Any field left null means "do not constrain on that column". Time bounds
+/// are half-open in the spec sense — is inclusive and
+/// is inclusive of the upper bound; the repository SQL uses
+/// >= / <= respectively. All filter fields are AND-combined.
+///
+public sealed record AuditLogQueryFilter(
+ AuditChannel? Channel = null,
+ AuditKind? Kind = null,
+ AuditStatus? Status = null,
+ string? SourceSiteId = null,
+ string? Target = null,
+ string? Actor = null,
+ Guid? CorrelationId = null,
+ DateTime? FromUtc = null,
+ DateTime? ToUtc = null);