Skip to content

Documentation

From nothing to a recorded change in five minutes.

The .NET path is below. Node.js, Python and Go follow the same shape.

How it fits together

Reldavi is two halves and it is worth knowing which is which before you install anything. The left of this drawing is code in your repository — three packages and about fifteen lines of configuration. The right runs somewhere else, either on our servers or on yours from the same compose file, and you do not build any of it.

WHAT YOU WRITE WHAT YOU DO NOT Your SaveChanges unchanged — you write nothing here The interceptor reads the change tracker, masks, never blocks A bounded buffer flushed on a background thread Ingestion API deduplicates a retried batch by id Event store columnar, partitioned per tenant and month Dashboard and query API search, timelines, exports, alerts one gzipped batch, every 2 seconds Three packages. About fifteen lines of configuration. Hosted by us, or the same compose file on your own servers.
The one hop that leaves your process is a gzipped batch every couple of seconds, on a background thread. Nothing on your request path waits for it. That is the whole design: an audit trail that can slow down a checkout is an audit trail somebody will eventually turn off.

What you need first

An ingest keyFrom Settings → API keys in the dashboard, or reldavi key --tenant <slug> --scopes ingest if you are self-hosting. It is shown once. Keep it in your configuration provider, not in appsettings.json — it is a credential, and it is the one that decides which tenant every event belongs to.
An endpointhttps://ingest.reldavi.com for the hosted service, or wherever you published the ingestion API.
.NET 8 or laterThe SDK multi-targets .NET 8 and .NET 10. You do not have to upgrade your application first.
Entity Framework CoreFor automatic capture. Without EF Core you can still record events by calling the client yourself, and the Node.js, Python and Go clients work that way by design.

1. Install

Two packages: the EF Core interceptor that captures changes, and the client that ships them.

dotnet add package Reldavi.EntityFrameworkCore
dotnet add package Reldavi.Client
dotnet add package Reldavi.AspNetCore
The SDK multi-targets .NET 8 and .NET 10. You do not need to upgrade your application first.

2. Configure

Register the client, attach the interceptor to your DbContext, and add the middleware that tells Reldavi who the current user is. The middleware goes after UseAuthentication, because before it the principal has no claims and every change would be attributed to nobody.

// Program.cs
builder.Services.AddReldaviClient(options =>
{
    options.Endpoint = new Uri("https://ingest.reldavi.com");
    options.ApiKey   = builder.Configuration["Reldavi:ApiKey"]!;
});

builder.Services.AddReldaviEntityFrameworkCore(options =>
{
    options.MaskHashSecret = builder.Configuration["Reldavi:MaskSecret"];
    options.MaskProperty("Iban", MaskStrategy.PreserveLast, 4);
});

builder.Services.AddDbContext<ShopDbContext>((sp, options) => options
    .UseNpgsql(connectionString)
    .UseReldavi(sp));

builder.Services.AddReldaviHttpContext();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
app.UseReldavi();   // after authentication

What each line does

AddReldaviClientThe transport: the endpoint, the key, the buffer size and the flush interval. Registers the background dispatcher that owns the only network call in the SDK.
AddReldaviEntityFrameworkCoreThe capture rules: masking secrets and any masks you would rather configure than annotate. Attributes and this do the same thing; use this for a property on a class you do not own.
.UseReldavi(sp)Attaches the interceptor to one DbContext. It goes on the context options, not on the service collection, and it needs the IServiceProvider overload — the parameterless one cannot resolve the sink, which is the single most common reason nothing appears.
AddReldaviHttpContextWhere the tenant and the actor come from: the authenticated principal on the current request. Omit it in a worker and push an AuditScope instead.
app.UseReldavi()The middleware that opens that scope per request. After UseAuthentication and UseAuthorization, because before them the principal has no claims and every change is attributed to nobody.

3. Annotate

By default every entity is audited and every property recorded. Attributes narrow that down. Nothing here changes your business logic.

Attribute
[AuditResource("shop.order")]Pins the public name. Rename the class later and saved filters keep working.
[AuditLabel]The property shown in the timeline, so a record reads as A-1042 rather than 91847.
[AuditMask(...)]Records a projection instead of the value, inside your process.
[AuditIgnore]Drops a property, or a whole entity, from the trail entirely.
[AuditResource("shop.order")]
public sealed class Order
{
    public int Id { get; set; }

    [AuditLabel]
    public string Reference { get; set; } = "";

    public string Status { get; set; } = "Pending";

    [AuditMask(MaskStrategy.PreserveLast, 4)]
    public string CardNumber { get; set; } = "";

    [AuditIgnore]
    public string InternalNotes { get; set; } = "";
}

4. Verify

Save a change and open the dashboard. The event appears within a couple of seconds — the default flush interval — and the diff shows only the properties that actually changed.

  • The event shows the right actor. If it says "anonymous", the middleware is registered before UseAuthentication.
  • Masked properties show their projection. If you see a raw value, the attribute is on the wrong member.
  • The reldavi.events.dropped counter is zero. If it is not, the buffer is too small or the endpoint is unhealthy.

What you have after five minutes

One save produces one event per entity it touched. This is what one of them looks like on the wire and in the store — it is worth reading once, because almost every question about the product afterwards is a question about one of these fields.

{
  "id":            "01937f2e-9c14-7a3b-8f21-6d5e4c3b2a19",
  "occurredAt":    "2026-09-21T14:12:08.4419Z",
  "resourceType":  "shop.order",
  "resourceId":    "91847",
  "resourceLabel": "A-1042",
  "action":        "updated",
  "actor": {
    "id":    "u_4410",
    "name":  "deniz@acme.test",
    "type":  "user"
  },
  "changes": {
    "Status":     { "from": "Pending",        "to": "Approved" },
    "Total":      { "from": 1240.00,          "to": 1116.00 },
    "CardNumber": { "from": "•••• •••• •••• 4417", "to": "•••• •••• •••• 9021" }
  }
}
idUUIDv7, generated in your process. It is also the idempotency key: a batch that was retried after a timeout deduplicates rather than doubling your history.
occurredAtWhen the save committed, from your clock, not when we received it. A queue that backs up for an hour must not move an event an hour later.
resourceType / resourceIdThe public name pinned by [AuditResource], and the primary key. Renaming the class later does not break a saved filter.
resourceLabelThe [AuditLabel] property, so a timeline reads as A-1042 rather than 91847.
actioncreated, updated, deleted — derived from the change tracker, not declared by you.
actorWho did it, resolved from the HTTP request. system for a background job, and an agent beside it when a model carried the change out.
changesOnly the properties that actually changed, each with its value before and after. An unchanged property is absent rather than repeated.
Masked properties arrive already masked. The raw value never existed outside the process that produced it — not in the buffer, not on the wire, not in our storage, and not in this payload.

What happens inside one save

Two questions arrive together the first time somebody reads the code: is my transaction slower now, and what happens to the audit event if it rolls back? Both answers are in the order of these four moments.

SavingChanges before the database sees anything read the tracker, mask, hold The write your transaction, unchanged nothing of ours runs here SavedChanges the commit succeeded hand the events to the buffer …or Failed rolled back, or cancelled discard everything captured A rolled-back save publishes nothing. The captured events are discarded with it.
Nothing of ours runs during your writeThe change tracker is read and the values are masked before the database is touched. What happens between there and the commit is your transaction, unchanged.
A rolled-back save leaves nothing behindSaveChangesFailed and a cancellation both discard everything captured. An audit trail that records changes that did not happen is worse than one that misses some — the first is wrong, the second is incomplete.
The interceptor holds no state between savesEF Core shares one instance across every context built from the same options. State for an in-flight save lives in a table keyed by the DbContext, which is why concurrent saves on different contexts cannot see each other's changes.
Handing over cannot blockPublishing to the buffer is a non-blocking write. If the buffer is full it sheds and increments reldavi.events.dropped rather than waiting, because the alternative is your checkout waiting on an audit log.

When your app is not an ASP.NET Core app

AddReldaviHttpContext resolves the tenant and the actor from the current HTTP request. A worker service, a console application or a message consumer has no request, so it tells Reldavi who is acting by pushing a scope instead. Everything else — the interceptor, masking, batching — is identical.

// A worker, a console app, a message consumer: no request, so push a scope.
using (AuditScope.Push(new AuditContext
{
    TenantId = "acme",
    Actor    = AuditActor.Service("nightly-reconciliation"),
}))
{
    order.Status = OrderStatus.Settled;

    await db.SaveChangesAsync(cancellationToken);
}
The actor is whoever is accountable, which for a background job is the service account it runs as rather than the person whose record it happens to be touching. If a model carried out the change, set Agent as well: it is recorded alongside the actor, never instead of it, because "was a person involved" has to stay answerable.

When nothing appears

Four things account for nearly every empty dashboard, in the order they are worth checking.

Nothing at all, and no errorsThe interceptor is not attached. UseReldavi(sp) goes on the DbContext options, not on the service collection, and it needs the IServiceProvider overload — the parameterless one cannot resolve the sink.
Events arrive, actor says `anonymous`UseReldavi() is registered before UseAuthentication(). Before authentication runs the principal has no claims, so every change is attributed to nobody. It goes after UseAuthorization().
A raw value where a mask should beThe attribute is on the wrong member — a backing field or a computed property rather than the mapped one. Masking is applied to what EF Core tracks.
`reldavi.events.dropped` is climbingThe buffer is full because the endpoint is unreachable or the batch size is too small for your write rate. Check reldavi.batches.failed beside it: failures mean the endpoint, zero failures mean the buffer.
Every one of these is visible from the metrics the SDK publishes, without turning on debug logging in production. They are listed under Observability.

The rest of the documentation

The quickstart above ends at a recorded change, which is the beginning rather than the whole of it. Everything the platform does is below, one page per subject.

Capturing changes Masking, recording an AI agent, the full configuration reference, and the other language clients.
Searching the trail The plain-language question box, conditions on the change itself, and turning a search into an alert.
Detection Per-account baselines, retention drift in both directions, and coverage — proof a producer that should be reporting still is.
Proving the trail Sealing and verification, evidence packs, legal hold, subject access requests and the auditor's room.
Access control Who read the trail, narrowing an account, the embedded viewer, and single sign-on.
Running Reldavi SIEM streaming, self-hosting, backup and restore, observability, rate limits and the two interface languages.

What to read next

Stuck on something?

The schema, the SDK sources and the full architecture notes are open. If something is unclear, that is a bug in our documentation. Get in touch.