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 need first
| An ingest key | From 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 endpoint | https://ingest.reldavi.com for the hosted service, or wherever you published the ingestion API. |
| .NET 8 or later | The SDK multi-targets .NET 8 and .NET 10. You do not have to upgrade your application first. |
| Entity Framework Core | For 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
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
AddReldaviClient | The 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. |
AddReldaviEntityFrameworkCore | The 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. |
AddReldaviHttpContext | Where 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.droppedcounter 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" }
}
}
id | UUIDv7, generated in your process. It is also the idempotency key: a batch that was retried after a timeout deduplicates rather than doubling your history. |
occurredAt | When 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 / resourceId | The public name pinned by [AuditResource], and the primary key. Renaming the class later does not break a saved filter. |
resourceLabel | The [AuditLabel] property, so a timeline reads as A-1042 rather than 91847. |
action | created, updated, deleted — derived from the change tracker, not declared by you. |
actor | Who did it, resolved from the HTTP request. system for a background job, and an agent beside it when a model carried the change out. |
changes | Only the properties that actually changed, each with its value before and after. An unchanged property is absent rather than repeated. |
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.
| Nothing of ours runs during your write | The 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 behind | SaveChangesFailed 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 saves | EF 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 block | Publishing 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);
}
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 errors | The 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 be | The 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 climbing | The 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. |
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
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.