Customer Audit UI

Give your customers visibility into their own audit logs without building a dashboard from scratch.

Why Offer Audit Logs to Customers?

Enterprise customers increasingly expect transparency. A customer-facing audit log helps you:

  • Win enterprise deals that require audit trail visibility
  • Reduce support tickets about "who did what"
  • Build trust through transparency
  • Meet compliance requirements (SOC 2, GDPR)

Option 1: Embedded Viewer (Recommended)

The fastest way to add audit logs to your app is our embeddable React component. It handles all the UI, filtering, and pagination.

Bash
1npm install @logvault/react
1import { AuditLogViewer } from '@logvault/react';
2
3function CustomerAuditPage({ customerId }) {
4 return (
5 <div className="p-8">
6 <h1>Activity Log</h1>
7 <AuditLogViewer
8 apiKey={process.env.LOGVAULT_PUBLIC_KEY}
9 actorId={customerId}
10 theme="light"
11 pageSize={25}
12 />
13 </div>
14 );
15}

Viewer Props

PropTypeDescription
apiKeystringYour LogVault public API key
actorIdstringFilter events by this actor ID
theme"light" | "dark"Color theme (default: "light")
pageSizenumberEvents per page (default: 25)
showFiltersbooleanShow action/date filters (default: true)
classNamestringAdditional CSS classes

Option 2: Build Your Own

If you need full control over the UI, use our API directly. Here's a minimal example:

1import { useState, useEffect } from 'react';
2
3function AuditLog({ customerId }) {
4 const [events, setEvents] = useState([]);
5 const [loading, setLoading] = useState(true);
6
7 useEffect(() => {
8 async function fetchEvents() {
9 const res = await fetch(
10 `/api/audit-events?actor_id=${customerId}`
11 );
12 const data = await res.json();
13 setEvents(data.events);
14 setLoading(false);
15 }
16 fetchEvents();
17 }, [customerId]);
18
19 if (loading) return <div>Loading...</div>;
20
21 return (
22 <table>
23 <thead>
24 <tr>
25 <th>Time</th>
26 <th>Action</th>
27 <th>Details</th>
28 </tr>
29 </thead>
30 <tbody>
31 {events.map(event => (
32 <tr key={event.id}>
33 <td>{new Date(event.timestamp).toLocaleString()}</td>
34 <td>{event.action}</td>
35 <td>{JSON.stringify(event.metadata)}</td>
36 </tr>
37 ))}
38 </tbody>
39 </table>
40 );
41}

Backend Proxy

Never expose your API key to the client. Create a backend endpoint that proxies requests:

JavaScript
1// pages/api/audit-events.js (Next.js)
2import { LogVault } from '@logvault/client';
3
4const client = new LogVault(process.env.LOGVAULT_API_KEY);
5
6export default async function handler(req, res) {
7 // Verify the user is authenticated
8 const session = await getSession(req);
9 if (!session) {
10 return res.status(401).json({ error: 'Unauthorized' });
11 }
12
13 // Only return events for the authenticated user's organization
14 const { actor_id } = req.query;
15
16 // Security: Ensure the actor_id belongs to this user's org
17 if (!canAccessActor(session.user, actor_id)) {
18 return res.status(403).json({ error: 'Forbidden' });
19 }
20
21 const events = await client.listEvents({
22 actorId: actor_id,
23 limit: 50
24 });
25
26 res.json({ events: events.data });
27}

Security Considerations

โš ๏ธ Important

Always filter events server-side. Never trust client-provided filters for access control. A customer should only see their own events.

  • Use a backend proxy - Never expose your API key to the browser
  • Verify ownership - Ensure the requested actor_id belongs to the authenticated user
  • Limit data exposure - Only return fields the customer should see
  • Rate limit - Protect your proxy endpoint from abuse

Customizing the Display

Map technical action names to human-readable labels:

JavaScript
1const actionLabels = {
2 'user.login': 'Signed in',
3 'user.logout': 'Signed out',
4 'document.created': 'Created document',
5 'document.deleted': 'Deleted document',
6 'settings.updated': 'Updated settings',
7 'payment.processed': 'Payment processed',
8};
9
10function formatAction(action) {
11 return actionLabels[action] || action;
12}

Example: Full Implementation

Here's a complete example with filtering, pagination, and nice styling:

1import { useState, useEffect } from 'react';
2import { formatDistanceToNow } from 'date-fns';
3
4const actionLabels = {
5 'user.login': { label: 'Signed in', icon: '๐Ÿ”' },
6 'user.logout': { label: 'Signed out', icon: '๐Ÿ‘‹' },
7 'document.created': { label: 'Created document', icon: '๐Ÿ“„' },
8 'settings.updated': { label: 'Updated settings', icon: 'โš™๏ธ' },
9};
10
11export function AuditLogViewer({ customerId }) {
12 const [events, setEvents] = useState([]);
13 const [loading, setLoading] = useState(true);
14 const [filter, setFilter] = useState('');
15 const [page, setPage] = useState(0);
16
17 useEffect(() => {
18 fetchEvents();
19 }, [customerId, filter, page]);
20
21 async function fetchEvents() {
22 setLoading(true);
23 const params = new URLSearchParams({
24 actor_id: customerId,
25 offset: page * 25,
26 limit: 25,
27 ...(filter && { action: filter })
28 });
29 const res = await fetch(`/api/audit-events?${params}`);
30 const data = await res.json();
31 setEvents(data.events);
32 setLoading(false);
33 }
34
35 return (
36 <div className="bg-white rounded-lg border">
37 <div className="p-4 border-b flex justify-between">
38 <h2 className="font-semibold">Activity Log</h2>
39 <select
40 value={filter}
41 onChange={e => setFilter(e.target.value)}
42 className="border rounded px-2 py-1"
43 >
44 <option value="">All activities</option>
45 <option value="user.*">Sign in/out</option>
46 <option value="document.*">Documents</option>
47 <option value="settings.*">Settings</option>
48 </select>
49 </div>
50
51 {loading ? (
52 <div className="p-8 text-center">Loading...</div>
53 ) : (
54 <ul className="divide-y">
55 {events.map(event => {
56 const meta = actionLabels[event.action] || {
57 label: event.action,
58 icon: '๐Ÿ“‹'
59 };
60 return (
61 <li key={event.id} className="p-4 flex items-center gap-4">
62 <span className="text-2xl">{meta.icon}</span>
63 <div className="flex-1">
64 <p className="font-medium">{meta.label}</p>
65 <p className="text-sm text-gray-500">
66 {formatDistanceToNow(new Date(event.timestamp))} ago
67 </p>
68 </div>
69 </li>
70 );
71 })}
72 </ul>
73 )}
74
75 <div className="p-4 border-t flex justify-between">
76 <button
77 onClick={() => setPage(p => Math.max(0, p - 1))}
78 disabled={page === 0}
79 >
80 Previous
81 </button>
82 <button onClick={() => setPage(p => p + 1)}>
83 Next
84 </button>
85 </div>
86 </div>
87 );
88}

Next Steps