Audkit is the append-only record of everything your product does. Every event is sequenced and committed to a per-project Merkle tree whose signed root is published where we can't reach it — so the entire history can be re-verified on demand, down to the first record.
npm i audkitmutation endpoints — the API cannot edit or delete
commits every event into an RFC 6962 Merkle tree
customer-held keys sign events before they leave your infra
signed verdict for every verify()
Authenticated and validated inside the request itself. No queue sits in the trust path.
A monotonic counter assigns the event its place. The next number is never in doubt.
The event's skeleton is hashed into a leaf and appended to the project's Merkle tree.
Safe aggregates flow to analytics. Payloads never leave Postgres.
The dashboard sees the event the moment it lands.
The new root is Ed25519-signed and published to Sigstore Rekor — somewhere we can't reach it.
the sealed fields
There is no edit endpoint to abuse — forging history means writing to the database directly. Even that shows: the altered row no longer hashes to the leaf committed under the signed root. verify() rebuilds the whole tree from sequence 1 and returns a signed receipt. When something is wrong, it names the exact sequence.
const receipt = await audit.verify();
receipt.valid; // false — someone touched row 88,412
receipt.checkedCount; // 184,201 leaves rebuilt, from sequence 1
receipt.firstBreak; // { sequence: 88412, reason: "leaf_mismatch" }
receipt.rootHash; // the root this rebuild produced. pin it.
receipt.signature; // the receipt itself is signed. check it.what a forgery looks like
Not just “something is wrong” — the exact sequence where the tree no longer holds.
Every verdict is signed by the platform. The proof of integrity can't be forged either.
Ask for an inclusion proof for one event, or a consistency proof between two roots. Both are checked by local hash arithmetic — the SDK or the CLI, offline.
Bring your own Ed25519 key. The SDK signs each event before it leaves your infrastructure, and only you hold the private key — a history not even the platform can forge.
Signed roots are published hourly to Sigstore Rekor, and digest emails carry the head. Truncate the record and the anchors disagree.
The platform's own control-plane actions land in project-audit — and verify the same way.
Personal or org-owned. The tree starts at sequence 1.
log:write · log:read · log:export · log:verify. Shown once, revocable always.
One call. The 202 comes back after the seal — sequenced and in the tree, not merely accepted.
import { Audkit } from "audkit";
const audit = new Audkit({ apiKey: process.env.AUDKIT_API_KEY! });
await audit.log({
action: "role.changed",
actor: { type: "user", id: "usr_9f27", display: "dana@acme.dev" },
target: { type: "member", id: "mem_512", display: "kai@acme.dev" },
metadata: { from: "member", to: "admin" },
});
// 202 — sequenced and committed to the tree before the ack. permanent.import { withAuditLogging } from "audkit/nextjs";
export const POST = withAuditLogging(
{
action: "invite.sent",
actor: async ({ request }) => {
// every resolver gets its own clone — reads never consume the body
const { user } = await auth.api.getSession({ headers: request.headers });
return { type: "user", id: user.id, display: user.email };
},
target: async ({ requestJson }) => {
const { email } = await requestJson();
return { type: "invite", id: email, display: email };
},
},
(request) => sendInvite(request),
);
// success or failed is read off the response. IP, user agent,
// and Vercel geo land in the event automatically.import { withAuditAction } from "audkit/nextjs";
export const deleteWorkspace = withAuditAction(
{
action: "workspace.deleted",
risk: "high",
actor: () => currentActor(),
target: ({ args: [id] }) => ({ type: "workspace", id }),
},
async (id) => workspaces.remove(id),
);
// redirect() logs success. a throw logs failed — then rethrows.import { auditedTool } from "audkit/ai";
const refund = auditedTool({
client: audit,
name: "refund_payment",
risk: "high",
requireReason: true,
authorize: ({ input }) => input.amountCents <= 50_00,
handler: ({ paymentId }) => payments.refund(paymentId),
});
// one sealed event per call —
// success, denied, or failed.$ npx audkit keygen
$ npx audkit verify full --export events.ndjson --root a8d772…
✓ full local rebuild reproduces the trusted root
// exit 0 — verified offline. no network. no trusting us.
// exit 1 means tamper detected, and names where.When events pass your retention window — 90 days by default, or never — the encrypted payload and the key that opens it are destroyed, and the storage shrinks. What stays is the leaf: a nonce-blinded commitment that still verifies under the signed root and tells nobody what was erased. Minimize data. Keep proof.
Build a hold from a query — actor, action, time range. Matching events are committed to a Merkle root anchored into the platform's own project-audit stream, with inclusion proofs on demand. Shredding is blocked for as long as the hold stands.
AI tools act in production now. auditedTool() wraps any AI-SDK tool with risk labels, required reasons, and an authorize gate — one event per call, success, denied, or failed.
end of record — you've reached the head
Create a project, mint a key, send an event. What lands in the record, stays in the record.