Audkit

Proof, not
promises.

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.

Start the recordnpm i audkit
0

mutation endpoints — the API cannot edit or delete

SHA-256

commits every event into an RFC 6962 Merkle tree

Ed25519

customer-held keys sign events before they leave your infra

1 receipt

signed verdict for every verify()

An event becomes permanent in six steps.

01accepted

Authenticated and validated inside the request itself. No queue sits in the trust path.

02sequenced

A monotonic counter assigns the event its place. The next number is never in doubt.

03committed

The event's skeleton is hashed into a leaf and appended to the project's Merkle tree.

04mirrored

Safe aggregates flow to analytics. Payloads never leave Postgres.

05broadcast

The dashboard sees the event the moment it lands.

06anchored

The new root is Ed25519-signed and published to Sigstore Rekor — somewhere we can't reach it.

the sealed fields

sequencegaps and reorderings have nowhere to hide
createdAtserver-assigned; bound into the leaf so time cannot drift
payloadHasha nonce-blinded commitment, built to outlive the payload itself
ciphertextHashpins the exact encrypted bytes we store for you
leafHashthe whole skeleton in one hash — its place under the root

Don't trust it. Check it.

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

#88,4095ad41e5dleaf verified
#88,41073ca62e2leaf verified
#88,411247ea620leaf verified
#88,41212b05d5fleaf_mismatch — row edited in place
#88,413e39133ebunchecked — verify() halts at the first break
#88,414cc1da3a2unchecked — verify() halts at the first break
firstBreak

Not just “something is wrong” — the exact sequence where the tree no longer holds.

signed receipts

Every verdict is signed by the platform. The proof of integrity can't be forged either.

proofs, not trust

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.

customer-held signing

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.

out-of-band anchors

Signed roots are published hourly to Sigstore Rekor, and digest emails carry the head. Truncate the record and the anchors disagree.

a second stream

The platform's own control-plane actions land in project-audit — and verify the same way.

First event in five minutes.

  1. 01create a project

    Personal or org-owned. The tree starts at sequence 1.

  2. 02mint a scoped key

    log:write · log:read · log:export · log:verify. Shown once, revocable always.

  3. 03send the first event

    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.

Compliance without holes in your history.

Crypto-shredded retention

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.

shred_expired_event_payloads()

Legal holds

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.

merkle root · inclusion proofs

Agents on the record

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.

audkit/ai

The rest of the instrument.

Logs explorerSearch by actor, action, target, status — an inclusion proof on every event.
Entity timelinesEverything an actor did. Everything done to a target.
Live activityEvents reach the dashboard the moment they seal.
AnalyticsVolume and breakdowns from a safe mirror. Aggregates only.
ExportsCSV or NDJSON — from the dashboard or an API key.
Route auditingwithAuditLogging() wraps a Next.js handler end to end.
Notification rulesA visual rule builder routing to email, webhook, Slack, Discord.
Email digestsDaily to monthly summaries, each carrying the signed tree head.

end of record — you've reached the head

Start the record.

Create a project, mint a key, send an event. What lands in the record, stays in the record.