Skip to content

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");
The hash strategy needs a stable secret. Without one, each process generates its own key, digests stop matching across restarts, and "find every record carrying this id" quietly stops working — a failure that surfaces months later, during an investigation.

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);
}
The agent is set alongside the actor, never instead of it. The actor stays whoever is accountable: the operator who started the run, or the service account it runs as. Folding the two together would make "was a person involved" unanswerable, and that is the question Article 14 asks.
autonomousThe agent acted by itself.
suggestedThe agent proposed the change; something else carried it out.
approvedA 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.

Nothing is forwarded to your SIEM but the digests and the lengths. A SIEM is indexed, replicated and widely readable inside an organisation, and mirroring a quarter of a megabyte of model input into it per event is a cost and a disclosure nobody asked for.

Configuration reference

OptionDefault
CaptureModeOptOutOptOut audits everything not marked [AuditIgnore]. OptIn audits only what is marked [AuditInclude].
MaskHashSecretKeys the hash strategy. Must be identical across every instance.
IncludeShadowPropertiestrueRecords EF shadow properties such as foreign keys. A reassigned CustomerId is usually worth recording.
CaptureFullSnapshotOnCreatetrueRecords the complete initial state of a new record.
MaxValueLength4096Longer string values are truncated with a marker.
BufferCapacity10000Events held before the client sheds load.
MaxBatchSize500Events per request.
FlushInterval2sHow 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

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.