Veriqa.Core.AuditTrail 0.6.1

Prefix Reserved
dotnet add package Veriqa.Core.AuditTrail --version 0.6.1
                    
NuGet\Install-Package Veriqa.Core.AuditTrail -Version 0.6.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Veriqa.Core.AuditTrail" Version="0.6.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Veriqa.Core.AuditTrail" Version="0.6.1" />
                    
Directory.Packages.props
<PackageReference Include="Veriqa.Core.AuditTrail" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Veriqa.Core.AuditTrail --version 0.6.1
                    
#r "nuget: Veriqa.Core.AuditTrail, 0.6.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Veriqa.Core.AuditTrail@0.6.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Veriqa.Core.AuditTrail&version=0.6.1
                    
Install as a Cake Addin
#tool nuget:?package=Veriqa.Core.AuditTrail&version=0.6.1
                    
Install as a Cake Tool

Veriqa.Core.AuditTrail

Satellite package holding the audit trail of the Veriqa core: the event-bus receiver, the append-only journal and the retention process. It is optional — a host that does not register it runs without an audit trail, and no core library references this package. The record model (AuditRecord, AuditMetadata) and the write-only sink contract (IAuditSink) live in the MIT-licensed Veriqa.Core.Contracts package, so an alternative journal can be written against them without this assembly.

The journal does not depend on the OIDC server: the keys of the Logging axis (Logging.Mode, Logging.RetentionDays) and the LoggingMode enum live in Veriqa.Core.Configuration, the settings mechanism both sides of the axis already depend on. Declaring those keys and binding their core level remain with the auth server assembly — declaring a key and owning its home are different acts — so a deployment that wires the journal without the auth server resolves the retention to nothing, and the sweep says so and skips the pass instead of deleting everything.

What gets written, and when

The receiver subscribes to the core's transaction event bus and is the only place that decides whether an event becomes a record. Publication itself is never filtered — the same bus drives the real-time sign-in UI. A record is created only when the effective Logging.Mode resolves to Audit; Disabled and System both mean "do not write", and there is no partial audit. The mode is resolved per event, so a transaction that outlives a mode switch leaves a partial trail by design.

Every record has the same six fields — timestamp, actor, action, target, result, metadata — and the metadata is a closed set of safe attributes. Channel identities appear masked; snapshot data and tokens never reach the journal. Issuing a token is not audited: that event does not travel the transaction bus, and nothing else writes to the journal.

Reading the journal

There is no read API, no endpoint and no UI — by contract, not by omission. An operator of a self-hosted deployment reads the journal with the tools of the deployment itself: for the EF Core sink, that is direct SQL access to the audit records table in the configured database. The in-memory sink is a development convenience with no durability at all.

Retention

Records are deleted only by the retention sweep, and only when they are older than the effective Logging.RetentionDays (default 90). The sweep runs regardless of the current mode: switching the mode off stops new records from appearing, it does not freeze the ones already written. How often it runs and in what batches is host-owned (ConfigureRetention); how long a record lives is not.

The sweep deletes through a contract that stays internal to this assembly — next to a write-only sink, a public delete contract would hand every consumer a way to erase audit records. A custom sink therefore cannot be swept by it, and is not: see the sink table below.

Known limitation: the sink shares the bus dispatcher

The receiver is an ordinary subscriber of the core's transaction event bus, and that bus has a single reader calling its handlers one after another. The journal therefore competes for the same dispatcher as the real-time sign-in notification: a slow (not failed) sink delays every handler queued behind it, and once the bounded publication queue fills up, the publishing request thread waits for a free slot. A failed sink is already contained — the receiver swallows its exceptions and the sign-in proceeds — but a slow one is not isolated, so the journal is off the critical path only as long as it keeps up.

Registration order is what decides who waits for whom: registering the audit trail after the handlers that drive the UI lets the browser be notified before the record is written. That removes the first hop of the delay, not the coupling itself — the next event still queues behind this record. Isolating the journal behind a reader of its own is deliberately not done: the core keeps one event dispatcher (SPEC-011 N26), and without a durable buffer a private queue would only move the back-pressure, while records may not be dropped (R33).

Actor of a decline: only the web page carries a masked identity

Only a refusal answered on the core web page reaches the journal with a masked channel identity in actor. A refusal answered inside the channel — and the same answer given on the email confirmation page — is recorded with the system actor and with empty channel_type and masked_channel_user_id.

This is the norm rather than a gap in it (SPEC-011 L34): attributing a decline is not a requirement of the journal, because the record is carried by the fact of the event and by the reason code in result, not by who answered. The actor is resolved from the attribution the event itself carries, and an actor is never invented to fill the field.

Where the difference comes from: a record carries only what the bus event carries, and the channel identity reaches an event solely by being attached to the transaction first. On these two paths nothing attaches it — the single operation that does also publishes an attachment event, which the real-time layer turns into an irreversible browser redirect to the confirmation page, for a transaction that is about to end. Attributing them would take a change to how events carry attribution, which the spec explicitly does not require (N26).

Relation to Serilog

Operational logs and the audit journal are two independent axes. Serilog is diagnostics, configured per deployment; exporting audit records through a Serilog sink as well is allowed, but the contractual guarantees — immutability, retention, access — are carried by the audit store only. An operational log is not a substitute for the journal.

Registration

builder.Services.AddVeriqaAuditTrail(audit =>
{
    audit.UseEfCoreSink(ef => ef.UseNpgsql(connectionString,
        npgsql => npgsql
            .MigrationsAssembly("Veriqa.Core.AuditTrail.Migrations.PostgreSql")
            .MigrationsHistoryTable("__AuditTrailMigrationsHistory")));
});

UseEfCoreSink itself ships in a satellite package — this package carries no ORM, so a deployment that keeps the in-process sink never receives EF Core. The migrations named in MigrationsAssembly(...) ship as one more package, of the same name as the assembly:

dotnet add package Veriqa.Core.AuditTrail.EntityFrameworkCore
dotnet add package Veriqa.Core.AuditTrail.Migrations.PostgreSql

The provider, the connection string and the migrations assembly are the host's choice. When the journal shares a database with another store, give it its own migration history table. Selecting no sink is a startup failure rather than a silent in-memory fallback: for a journal, losing records quietly is worse than refusing to start.

Choosing the sink

Exactly one sink is chosen, and what it is decides who owns the retention:

Call Sink Retention
UseInMemorySink() in-process, development and running without a database the built-in sweep
UseEfCoreSink(...) (the Veriqa.Core.AuditTrail.EntityFrameworkCore package) the audit table of the configured database the built-in sweep
UseSink<TSink>() any IAuditSink implementation of yours yours — no sweep is registered, and the host says so at startup with an Information record
builder.Services.AddVeriqaAuditTrail(audit => audit.UseSink<MyAuditSink>());

Choosing a built-in sink and a custom one in the same registration is refused on the spot: the records would go to the custom sink while the sweep kept clearing the built-in store. The same refusal covers any IAuditSink registered after a built-in sink — through AuditTrailBuilder.Services or by a following AddVeriqaAuditTrail call — for the same reason. Registering one before the built-in sink is not refused: the built-in sink is then the last registration, and it owns both the records and their deletion. Calling ConfigureRetention(...) next to a custom sink is not an error — the parameters simply have no sweep to configure, which the same startup record states.

A registration can only be refused while it is still a registration: an IAuditSink put into the host's own IServiceCollection after AddVeriqaAuditTrail(...) has returned is invisible to that check. It is caught one step later instead — the sweep compares the sink the container resolved against the store it deletes from, and starts with a Warning record when the two have come apart. A sink that wraps the built-in one is a different instance too and gets the same record, although it writes through to the swept store; the notice says so.

Calling AddVeriqaAuditTrail(...) more than once on the same collection is allowed, and the retention parameters are shared between the calls: a later call that never touches ConfigureRetention(...) leaves what an earlier one set in place, and one that does configures the sweep already registered.

An ecosystem package can extend the registration surface itself: AuditTrailBuilder.Services is public (hidden from IntelliSense), so an extension method on the builder registers whatever the sink needs — its options, its clients, its hosted services — inside the same AddVeriqaAuditTrail call.

License

Mozilla Public License 2.0 — see the LICENSE file in the repository root; the package carries the MPL-2.0 license expression in its metadata.


Veriqa™ — © Dmitrii Erusov. Sources and mirrors: https://veriqa.app · devs@veriqa.app

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Veriqa.Core.AuditTrail:

Package Downloads
Veriqa.Core.AuditTrail.EntityFrameworkCore

EF Core audit sink for the Veriqa audit trail (satellite package). The provider (Npgsql etc.) and the migrations assembly belong to the host.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.6.1 109 10/7/2026
0.6.0 143 9/23/2026