Documentation
What is captured, and what never leaves your process.
Capture runs inside your SaveChanges, so the rules about what it may do are stricter than the rules about anything else here.
Masking
Masks run in your process before anything is buffered. Apply them per property with an attribute, or model-wide by name when the same field appears on many entities.
// Per property
[AuditMask(MaskStrategy.PreserveLast, 4)] // ************4242
[AuditMask(MaskStrategy.Hash)] // sha256:9f2b1c74a0e8…
[AuditMask(MaskStrategy.Email)] // e***@contoso.com
[AuditMask(MaskStrategy.Length)] // ***(16)
[AuditMask] // ***
// Model-wide, by property name
options.MaskProperty("NationalId", MaskStrategy.Hash);
options.IgnoreProperty("PasswordHash");
Recording an AI agent
When a model carries out the change rather than a person, the record has to say so. Set the agent on the scope and every change the save produces carries it — an agent is handed a task, not a property, and one run may touch six records.
using (AuditScope.Push(new AuditContext
{
Actor = new AuditActor(user.Id, "user", user.FullName),
Agent = new AuditAgent(
"claude-opus-5",
ModelVersion: "2026-05-01",
Tool: "issue_refund",
Decision: AuditAgent.Approved,
OnBehalfOf: user.Id),
Payload = AuditPayload.Create(prompt, answer),
}))
{
// Every change in here is attributed to the agent.
await db.SaveChangesAsync(ct);
}
| autonomous | The agent acted by itself. |
| suggested | The agent proposed the change; something else carried it out. |
| approved | A person confirmed the change before it happened. |
The prompt and the answer are recorded outside the change set, in their own columns, with a far larger bound than a property value gets. Article 12 asks for the input data and the result, and half a prompt proves nothing about what the system was asked. The digest is always taken over the whole of each side before anything is truncated, so a shortened record can still be matched against a full copy held elsewhere.
Configuration reference
| Option | Default | |
|---|---|---|
| CaptureMode | OptOut | OptOut audits everything not marked [AuditIgnore]. OptIn audits only what is marked [AuditInclude]. |
| MaskHashSecret | — | Keys the hash strategy. Must be identical across every instance. |
| IncludeShadowProperties | true | Records EF shadow properties such as foreign keys. A reassigned CustomerId is usually worth recording. |
| CaptureFullSnapshotOnCreate | true | Records the complete initial state of a new record. |
| MaxValueLength | 4096 | Longer string values are truncated with a marker. |
| BufferCapacity | 10000 | Events held before the client sheds load. |
| MaxBatchSize | 500 | Events per request. |
| FlushInterval | 2s | How long a partial batch waits before being sent. |
Other languages
Node.js, Python and Go clients post the same document as the .NET SDK. They capture manually rather than through an ORM hook, and each ships a diff helper that records only the properties that actually changed.
// Node.js — npm install @reldavi/client
import { ReldaviClient, mask, diff } from '@reldavi/client';
const audit = new ReldaviClient({
endpoint: 'https://ingest.reldavi.com',
apiKey: process.env.RELDAVI_API_KEY,
});
audit.capture({
action: 'updated',
resourceType: 'shop.order',
resourceId: order.id,
actor: { id: user.id, displayName: user.name, ip: req.ip },
changes: mask.applyMasks(diff(before, after), {
cardNumber: mask.preserveLast(4),
}),
});
await audit.close(); // on shutdown
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.