Skip to content

Documentation

Running it, on our servers or yours.

The same compose file either way. Nothing here is a hosted-only feature, because a compliance product a company cannot run itself is one it cannot audit either.

Streaming to a SIEM

Audit records are also security telemetry, and the team that correlates them already has a tool. Each stored batch is mirrored into it — mirroring, not moving: the event store stays the record.

SplunkHTTP Event Collector. Each event in its own envelope, with epoch seconds and your index.
Microsoft SentinelThe Log Analytics data collector endpoint, into the custom table you name.
Elasticsearch / OpenSearchThe bulk API, with create and the event id as the document id — so a replayed batch is rejected as a duplicate rather than silently overwriting.
WebhookAny HTTPS endpoint, receiving newline-delimited JSON.

One field shape across all four. Names match the export and the query API rather than each vendor’s house style, so a customer correlating against their firewall logs writes the field name once, and moving from Splunk to Elastic does not invalidate their saved searches.

# Generate the key once, then give it to both the dashboard and the processor.
dotnet run --project tools/Reldavi.Cli -- siem-key

Siem__Enabled=true
Siem__CredentialKey=<32 random bytes, base64>

Collector tokens are encrypted at rest with a key that lives only in configuration. The dashboard and the processor must hold the same one, and losing it loses the stored tokens rather than any audit data — they have to be re-entered.

Included in every paid plan rather than metered per connection. The marginal cost of a second connection is an HTTP client.

Self-hosting

One compose file brings up PostgreSQL, ClickHouse, the message broker, both APIs and the dashboard. On first start it creates your tenant, your owner account and an ingest key, and prints the key once.

cd deploy
cp .env.example .env          # change every secret in it
docker compose up -d

# The ingest key is printed once, into the ingestion service's log.
docker compose logs ingestion-api | grep "Ingest API key"
Nothing in the stack calls home. There is no telemetry, no licence check and no CDN dependency, so it runs air-gapped.

Backup and restore

Self-hosting means the copies are yours to keep. There is one thing about this platform that makes it different from backing up an ordinary application, and it is worth knowing before you need it rather than after.

Your events live in ClickHouse. The checkpoints that prove those events — the Merkle roots, the timestamp anchors — live in PostgreSQL. Separately neither is worth much: events without checkpoints are rows nobody can show were not edited, and checkpoints without events are hashes of nothing. So a partial restore does not degrade gracefully. It produces a trail that looks complete, serves normally, and quietly proves nothing.
Both databases, one backupbuild/backup.sh takes them together and writes a manifest with the counts it saw, so a restore has something to compare against.
And the secrets beside themSiem__CredentialKey decrypts every stored SIEM collector token. Lose it and every tenant re-enters theirs by hand. It is in .env, not in either database.
Restore the control plane firstEither order works while both halves succeed. This one is chosen so that a failure in the second half leaves you knowing the proof survived.
Then prove itThe restore is not finished when the data is back. It is finished when reldavi verify says every sealed day still hashes to its checkpoint — and nothing else will tell you.

The full procedure, including the drill to run before you rely on any of it, is in docs/runbook-backup-restore.md in the repository. Evidence packs are deliberately not backed up: a pack is an output and can be written again from the two databases, and one already handed to an auditor lives wherever the auditor keeps it.

Observability

The SDK exposes OpenTelemetry metrics. Register the meter and alert on the first row.

builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .AddMeter(ReldaviDiagnostics.MeterName));
Metric
reldavi.events.droppedEvents lost because the buffer was full. Alert on this. Non-zero means you are losing audit coverage.
reldavi.capture.durationTime spent inside the interceptor, on your thread. This is the "no added latency" promise, measured.
reldavi.events.deliveredEvents acknowledged by the ingestion endpoint.
reldavi.batches.failedBatches abandoned after exhausting retries.

Rate limits and backpressure

The ingestion endpoint is rate-limited per tenant: a token bucket, 50 requests per second sustained with a burst of 200, and each request may carry up to ten thousand events. The limit bounds request overhead rather than how much you may audit — at the maximum batch size it is half a million events a second, per tenant.

Partitioned by tenant, not by IPEvery request from your fleet arrives from a handful of NAT addresses, so an IP partition would either throttle you as one client or fail to isolate you from anybody else.
A token bucket, not a fixed windowAudit traffic is bursty by nature — a nightly job writes ten thousand rows in a second. A fixed window would reject that burst while your average rate sat far below the limit.
A refusal carries `Retry-After`Which is what turns a limit into backpressure rather than an outage. Without it a well-behaved client retries immediately and makes the overload worse. Every SDK here honours it.
Nothing is lost to a 429The client keeps the batch and retries it. What loses events is a buffer that fills faster than the endpoint drains for long enough — and that is counted and alertable rather than silent.

Self-hosting? These are configuration, under RateLimiting. The defaults are chosen for a shared deployment where one tenant must not be able to starve the others; on your own hardware with one tenant, raise them.

English and Turkish

Not a translated marketing site with an English product behind it. The dashboard, the exports, the evidence pack and the verification instructions inside it are all written in both, and each person picks their own — the same tenant can have an English-reading engineer and a Turkish-reading compliance officer looking at the same trail.

Search folds case across the whole alphabetSearching öztürk finds Mehmet Öztürk. ASCII-only case folding — the default in a surprising number of systems — leaves Ö and ö as different letters, so a Turkish name is findable only by guessing its capitalisation. It fails in the way nobody reports, too: it does not error, it just returns fewer rows than it should. A walkthrough searches the lowercase form of a name taken from the seeded data and expects the row back.
The evidence pack is bilingualHOW-TO-VERIFY.txt carries both, so the pack can be handed to an auditor without anybody translating a verification procedure by hand.
Dates and numbers follow the readerNot the server. A timestamp an auditor reads in the wrong order is a timestamp that starts an argument.
KVKK obligations are written in Turkish and audited in Turkish. A compliance tool whose evidence comes out in English is one more thing somebody has to translate under time pressure, on the day it matters most.

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.