On this page
The audit log page is now a component
Enterprise customers do not just ask whether you have an audit log. They ask where it is. And the answer they want is: right there in your product, under Settings.
That page is now something you can install. @invoance/audit-viewer is the viewer that powers our hosted audit portal, published as an npm package. Mount it on your admin page (say, admin.yourapp.com/audit) and each of your customers gets a full audit experience inside your product: the signed event table, filters by actor, action, and date, CSV/JSON export, and live updates as new events arrive.
It renders as a native React component in your DOM: no iframe, no styling conflicts. And because it is literally the same component the hosted portal mounts, whether you hand a security team a one-time link or embed the page natively, your customers see the same thing and the proof works the same way.
npm install @invoance/audit-viewer
What your customers see
The event table shows actor, targets, action, and gap-free sequence numbers, with filters, keyset paging, export, and a quiet 15-second refresh while the page is idle.
The part we care most about is the detail drawer. Click any row and your customer sees the raw signed event next to a Verify signature button. The verdict reads Authentic. Then they can edit any field in the editor, change one character of the payload, and verify again: it flips to Tampered, computed entirely in their browser with the tenant's published public key. Trust that does not depend on taking anyone's word for it, including ours.
The same component also doubles as a stream configuration screen: mint the session with intent log_streams instead of audit_logs and it renders the SIEM webhook destination setup rather than the event table, so "stream your logs to your own security tooling" is a page you can offer without building it either.
One server-side piece: minting the session
The component authenticates with a short-lived, org-scoped portal session that your backend mints for whichever of your customers is signed in. This is the only server-side work.
// app/api/audit-portal-token/route.ts
import { InvoanceClient } from "invoance";
// Reads INVOANCE_API_KEY from the environment. The key stays on the server.
const client = new InvoanceClient();
export async function POST() {
const organizationId = await currentCustomerOrgId(); // however your app resolves it
const session = await client.audit.portalSessions.create({
organizationId,
intent: "audit_logs",
sessionDurationSeconds: 3600,
});
return Response.json({ token: session.token });
}
Mount it
On the client, pass a callback that fetches a fresh token from that endpoint. It is a callback rather than a static prop on purpose: when the session expires, the component re-mints through your endpoint automatically, so expiry never strands the visitor.
"use client";
import { AuditLogViewer } from "@invoance/audit-viewer";
import "@invoance/audit-viewer/styles.css";
export default function AuditPage() {
return (
<AuditLogViewer
getPortalToken={async () => {
const r = await fetch("/api/audit-portal-token", { method: "POST" });
return (await r.json()).token;
}}
/>
);
}
Theming and fit
Nine CSS variables (HSL triples, shadcn-style) drive every color in the component. Override them on :root, on any ancestor, or through the theme prop; bare names get the --inv- prefix automatically. A dark embed that matches your product is a handful of lines, so the viewer reads as your page, not a widget from someone else.
<AuditLogViewer
getPortalToken={getPortalToken}
theme={{ variables: { background: "240 6% 12%", foreground: "0 0% 98%", border: "240 6% 20%", muted: "240 5% 16%", popover: "240 6% 12%" } }}
/>
The failure modes are handled
The exchanged session is org-scoped: it can only ever read the one organization it was minted for, and your tenant id is never exposed to the visitor. Portal traffic is rate-limited per visitor IP and never counts against your API budget.
On a 401 mid-session the component calls getPortalToken once and retries; if that fails it shows a clear expiry message and calls onSessionExpired. A rate-limited token exchange (many visitors behind one office NAT clicking at once) retries automatically honoring Retry-After, and a one-time link is not consumed by a rate-limit rejection.
In browsers whose Web Crypto implementation lacks Ed25519, the tamper test still checks the event hash and reports the signature as not verifiable in this browser, explicitly distinct from a failed verification, so nobody mistakes a browser limitation for tampering.
Get started
If you already send Invoance events, embedding the viewer is an afternoon of work: install the package, add the token endpoint, mount the component. If you do not yet, the free tier covers 10,000 events a month, and the Clerk and Auth0 integrations will fill a log with zero code while you evaluate. The full guide, covering props, theming variables, and session semantics, lives in the developer docs.
