Node SDK · Recorder
With the recorder constructed and the middleware wired, your handlers can record events. The examples below are lifted from the secrets-vault examples repo.
Simple write
The most common case — one event per request. Set fields on req.event and the middleware records on response finish. Handlers that don't set req.event.action are skipped entirely (no audit noise):
app.post("/api/secrets/:id/reveal", (req, res) => {
const me = actor(req);
if (!me) {
res.status(401).send("unknown actor");
return;
}
req.event!.tenantId = me.tenantId;
req.event!.target = { type: "secret", id: req.params.id };
const s = vault.get(req.params.id);
if (!s || s.tenantId !== me.tenantId) {
res.status(404).send("not found");
return;
}
req.event!.action = "secret.reveal";
req.event!.withFields("name", s.name);
res.json({ value: s.value });
});
The middleware auto-fills result from the response status, so error paths and success paths both record correctly without you setting result by hand.
Mutation with before/after diff
For events that change a resource, populate the change field so the audit trail shows what changed, not just that something did:
app.post("/api/secrets/:id/rotate", (req, res) => {
const me = actor(req)!;
req.event!.tenantId = me.tenantId;
req.event!.target = { type: "secret", id: req.params.id };
const { value } = req.body ?? {};
const s = vault.get(req.params.id);
if (!s) {
res.status(404).send("not found");
return;
}
const before = snapshot(s); // pre-mutation state
s.value = value;
s.rotatedAt = new Date().toISOString();
const after = snapshot(s); // post-mutation state
req.event!.action = "secret.rotate";
req.event!.diff(before, after); // populates req.event.change with the diff
res.status(204).send();
});
snapshot is your function — typically a struct that mirrors the resource. The diff helper computes the JSON patch between the two snapshots and stores it in event.change.
Redacted fields
req.event!.diff accepts a withRedactedFields(...) option that replaces sensitive paths with "[REDACTED]" before the diff is stored. Paths are JSON pointers:
import { withRedactedFields } from "@everscribe/sdk-node/event";
req.event!.diff(before, after,
withRedactedFields("/passwordHash", "/billing/creditCard"));
Two approaches, pick whichever fits the resource shape:
- Redact paths at diff time with
withRedactedFields— shorter when you have a small known set of paths to strip from an otherwise-safe struct. - Pre-redact in your
snapshotfunction — safer for resources where the sensitive field is large, structured, or easy to forget. The vault examples take this approach:snapshot()always returnsvalue: "[REDACTED]"so the plaintext never even reaches the SDK.
Batch recording
The buffered recorder batches events under the hood. record() enqueues; a background loop sends batches every flushInterval or when flushSize is reached, whichever comes first. You don't need to call any batch API.
For high-throughput services, tune the batch behavior via constructor options:
const rec = es.newRecorder({
bufferSize: 5000,
flushSize: 500,
flushInterval: 2_000,
});
For graceful shutdown, drain the buffer so pending events make it to the API before the process exits:
await rec.close(); // flushes pending events, then disposes
Multiple events per request
Some handlers naturally produce more than one event — sharing a secret with three recipients, fan-out notifications, bulk imports. Call fromContext(req) once per event you want to record. Each call returns an independent clone of the per-request template (actor, origin pre-filled); record each clone explicitly. The recorder batches them together on the next flush.
import { fromContext } from "@everscribe/sdk-node/event";
app.post("/api/secrets/:id/share", (req, res) => {
const me = actor(req)!;
const id = req.params.id;
const { userIds = [] } = req.body ?? {};
vault.share(me.tenantId, id, userIds);
// One audit event per recipient. Each fromContext() call yields a
// fresh clone with actor + origin already filled in.
for (const uid of userIds) {
const e = fromContext(req);
e.tenantId = me.tenantId;
e.action = "secret.share";
e.target = { type: "secret", id };
e.withFields("shared_with", uid);
rec.record(e);
}
res.status(204).send();
});
Direct recording
Not every event comes from an HTTP request. Cron jobs, queue workers, CLI scripts, startup hooks — anywhere the audit middleware isn't in the path, you build each event yourself: set actor to a service identity and set result by hand since there's no response status to derive it from.
// rotateExpiringSecrets is a scheduled job that rotates any secret due
// for rotation. Runs out of band — no middleware, no request, just the
// recorder you constructed at boot.
async function rotateExpiringSecrets(rec: BufferedRecorder, vault: Vault) {
for (const s of vault.dueForRotation()) {
const e: Event = {
action: "secret.rotate",
actor: { type: "service", id: "rotation-worker" },
target: { type: "secret", id: s.id },
result: { status: "ok" },
};
try {
await vault.rotate(s.id, generate());
} catch (err) {
e.result = { status: "error", message: String(err) };
}
await rec.record(e);
}
}
The same recorder instance backs both your Express handlers and your background jobs — events from both paths land in the same project and batch together. Construct one recorder at boot, share it across both surfaces, and close() it once on shutdown.