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.
1npm install @logvault/react
1import { AuditLogViewer } from '@logvault/react';23function CustomerAuditPage({ customerId }) {4 return (5 <div className="p-8">6 <h1>Activity Log</h1>7 <AuditLogViewer8 apiKey={process.env.LOGVAULT_PUBLIC_KEY}9 actorId={customerId}10 theme="light"11 pageSize={25}12 />13 </div>14 );15}
Viewer Props
| Prop | Type | Description |
|---|---|---|
apiKey | string | Your LogVault public API key |
actorId | string | Filter events by this actor ID |
theme | "light" | "dark" | Color theme (default: "light") |
pageSize | number | Events per page (default: 25) |
showFilters | boolean | Show action/date filters (default: true) |
className | string | Additional 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';23function AuditLog({ customerId }) {4 const [events, setEvents] = useState([]);5 const [loading, setLoading] = useState(true);67 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]);1819 if (loading) return <div>Loading...</div>;2021 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:
1// pages/api/audit-events.js (Next.js)2import { LogVault } from '@logvault/client';34const client = new LogVault(process.env.LOGVAULT_API_KEY);56export default async function handler(req, res) {7 // Verify the user is authenticated8 const session = await getSession(req);9 if (!session) {10 return res.status(401).json({ error: 'Unauthorized' });11 }1213 // Only return events for the authenticated user's organization14 const { actor_id } = req.query;1516 // Security: Ensure the actor_id belongs to this user's org17 if (!canAccessActor(session.user, actor_id)) {18 return res.status(403).json({ error: 'Forbidden' });19 }2021 const events = await client.listEvents({22 actorId: actor_id,23 limit: 5024 });2526 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:
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};910function 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';34const 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};1011export 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);1617 useEffect(() => {18 fetchEvents();19 }, [customerId, filter, page]);2021 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 }3435 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 <select40 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>5051 {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))} ago67 </p>68 </div>69 </li>70 );71 })}72 </ul>73 )}7475 <div className="p-4 border-t flex justify-between">76 <button77 onClick={() => setPage(p => Math.max(0, p - 1))}78 disabled={page === 0}79 >80 Previous81 </button>82 <button onClick={() => setPage(p => p + 1)}>83 Next84 </button>85 </div>86 </div>87 );88}