Audit
The Audit plugin provides comprehensive audit logging for all CRUD operations performed on your resources. It automatically tracks who performed what operations, when they were performed, and captures the data changes for compliance and debugging purposes.
Installation
Add the plugin to your query config
To use the Audit plugin, add it to your query config.
import { betterQuery } from "better-query"
import { auditPlugin } from "better-query/plugins"
export const query = betterQuery({
// ... other config options
plugins: [
auditPlugin({
enabled: true,
operations: ["create", "update", "delete"], // Operations to audit
includeData: true, // Store the actual data being modified
retention: 90, // Keep audit logs for 90 days
})
]
})Migrate the database
Run the migration or generate the schema to add the necessary audit tables to the database.
npx better-query migratenpx better-query generateSee the Schema section to add the fields manually.
Add the client plugin
Client Usage
The audit plugin automatically exposes server endpoints that you can access through your regular query client. No additional client configuration is needed.
import { createQueryClient } from "better-query/client"
export const queryClient = createQueryClient({
baseURL: "http://localhost:3000/api"
})
// Access audit logs via the plugin endpoint
const auditLogs = await queryClient.$fetch('/audit/logs', {
query: {
resource: "product",
operation: "create",
page: 1,
limit: 20
}
});Usage
The audit plugin automatically captures all CRUD operations. No additional code is required in your resource operations - the plugin handles everything in the background.
Viewing Audit Logs
Retrieve audit logs for analysis and compliance reporting.
const { data: auditLogs, error } = await authClient.audit.logs({ resourceType: "product", operation: "update", userId: "user-123", resourceId: "product-456", startDate: new Date("2024-01-01"), endDate: new Date("2024-12-31"), page: 1, limit: 50,});| Prop | Description | Type |
|---|---|---|
resourceType? | Filter by resource type | string |
operation? | Filter by operation type | "create" | "read" | "update" | "delete" | "list" |
userId? | Filter by user who performed the operation | string |
resourceId? | Filter by resource ID that was affected | string |
startDate? | Date range filter - start date | Date |
endDate? | Date range filter - end date | Date |
page? | Pagination | number |
limit? | Number of results per page | number |
Client Usage:
import { queryClient } from "@/lib/query-client"
// Get all audit logs
const auditLogs = await queryClient.audit.list({
page: 1,
limit: 20
})
// Get audit logs for a specific resource type
const productAuditLogs = await queryClient.audit.list({
resourceType: "product",
operation: "update"
})
// Get audit logs for a specific user
const userAuditLogs = await queryClient.audit.list({
userId: "user-123",
startDate: new Date("2024-01-01"),
endDate: new Date("2024-01-31")
})Resource Audit History
Get the complete audit history for a specific resource.
const { data: resourceHistory, error } = await authClient.audit.resource.:resourceid({ resourceId: "product-456", // required resourceType: "product", // required includeData: true,});| Prop | Description | Type |
|---|---|---|
resourceId | The ID of the resource | string |
resourceType | The type of resource | string |
includeData? | Include the actual data changes | boolean |
Client Usage:
import { queryClient } from "@/lib/query-client"
// Get complete history of a product
const productHistory = await queryClient.audit.getResourceHistory("product-456", {
resourceType: "product",
includeData: true
})
// The result includes all changes made to the resource over time
console.log(productHistory.data)
// [
// {
// id: "audit-1",
// operation: "create",
// timestamp: "2024-01-15T10:30:00Z",
// userId: "user-123",
// resourceType: "product",
// resourceId: "product-456",
// data: { name: "T-Shirt", price: 29.99 },
// changes: null // null for create operations
// },
// {
// id: "audit-2",
// operation: "update",
// timestamp: "2024-01-16T14:20:00Z",
// userId: "user-124",
// resourceType: "product",
// resourceId: "product-456",
// data: { name: "Premium T-Shirt", price: 34.99 },
// changes: {
// name: { from: "T-Shirt", to: "Premium T-Shirt" },
// price: { from: 29.99, to: 34.99 }
// }
// }
// ]Audit Statistics
Get audit statistics and insights.
Client Usage:
import { queryClient } from "@/lib/query-client"
// Get audit statistics for the last 30 days
const auditStats = await queryClient.audit.getStats({
startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
endDate: new Date(),
groupBy: "operation" // or "resourceType", "userId", "day"
})
console.log(auditStats.data)
// {
// total: 156,
// breakdown: {
// create: 45,
// update: 78,
// delete: 12,
// read: 21
// }
// }Configuration
Plugin Options
Configure the audit plugin behavior:
auditPlugin({
/**
* Enable/disable audit logging
*/
enabled: true,
/**
* Which operations to audit
*/
operations: ["create", "update", "delete"],
/**
* Whether to include the actual data in audit logs
*/
includeData: true,
/**
* Whether to capture data changes (before/after for updates)
*/
captureChanges: true,
/**
* Retention period in days (0 = keep forever)
*/
retention: 90,
/**
* Resources to audit (empty = all resources)
*/
resources: [], // or ["product", "user"] to limit to specific resources
/**
* Custom audit data transformer
*/
transform: (auditData) => {
// Add custom fields or modify audit data
return {
...auditData,
environment: process.env.NODE_ENV,
ipAddress: auditData.context?.request?.ip,
}
},
/**
* Custom storage for audit logs (optional)
*/
storage: {
// Implement custom storage adapter if needed
save: async (auditLog) => { /* custom save logic */ },
query: async (filters) => { /* custom query logic */ },
}
})Resource-Level Configuration
You can also configure audit settings per resource:
import { betterQuery, createResource } from "better-query"
export const query = betterQuery({
plugins: [auditPlugin()],
resources: [
createResource({
name: "product",
schema: productSchema,
audit: {
operations: ["create", "update", "delete"], // Override global settings
includeData: false, // Don't store sensitive product data
retention: 365, // Keep product audits for 1 year
}
}),
createResource({
name: "order",
schema: orderSchema,
audit: {
operations: ["create", "update"], // Only audit creates and updates
includeData: true,
captureChanges: true,
}
})
]
})Schema
The audit plugin creates the following database table:
CREATE TABLE audit_logs (
id TEXT PRIMARY KEY,
operation TEXT NOT NULL, -- 'create', 'read', 'update', 'delete', 'list'
resource_type TEXT NOT NULL,
resource_id TEXT,
user_id TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
data TEXT, -- JSON string of the resource data
changes TEXT, -- JSON string of before/after changes
metadata TEXT, -- Additional context like IP address, user agent, etc.
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Indexes for efficient querying
CREATE INDEX idx_audit_logs_resource ON audit_logs(resource_type, resource_id);
CREATE INDEX idx_audit_logs_user ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_timestamp ON audit_logs(timestamp);
CREATE INDEX idx_audit_logs_operation ON audit_logs(operation);React Hook Example
Here's a React hook for easily accessing audit data in your components:
import { useState, useEffect } from 'react'
import { queryClient } from '@/lib/query-client'
export function useAuditLogs(filters = {}) {
const [auditLogs, setAuditLogs] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
const fetchAuditLogs = async () => {
setLoading(true)
try {
const result = await queryClient.audit.list(filters)
if (result.data) {
setAuditLogs(result.data.items)
} else {
setError(result.error)
}
} catch (err) {
setError(err)
} finally {
setLoading(false)
}
}
fetchAuditLogs()
}, [JSON.stringify(filters)])
return { auditLogs, loading, error }
}
export function useResourceAuditHistory(resourceId: string, resourceType: string) {
const [history, setHistory] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
const fetchHistory = async () => {
setLoading(true)
const result = await queryClient.audit.getResourceHistory(resourceId, {
resourceType,
includeData: true
})
if (result.data) {
setHistory(result.data)
}
setLoading(false)
}
if (resourceId && resourceType) {
fetchHistory()
}
}, [resourceId, resourceType])
return { history, loading }
}Usage in components:
import { useAuditLogs } from '@/hooks/use-audit'
export function AuditLogViewer() {
const { auditLogs, loading, error } = useAuditLogs({
resourceType: 'product',
operation: 'update'
})
if (loading) return <div>Loading audit logs...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div>
<h2>Product Update Audit Logs</h2>
{auditLogs.map(log => (
<div key={log.id} className="audit-entry">
<p><strong>{log.operation}</strong> on {log.resourceType} {log.resourceId}</p>
<p>By user: {log.userId} at {new Date(log.timestamp).toLocaleString()}</p>
{log.changes && (
<details>
<summary>View Changes</summary>
<pre>{JSON.stringify(log.changes, null, 2)}</pre>
</details>
)}
</div>
))}
</div>
)
}Integration with Better Auth
When using the audit plugin with Better Auth, user information is automatically captured from the context:
import { betterQuery, createResource } from "better-query"
import { auditPlugin } from "better-query/plugins"
import { auth } from "./auth" // Your Better Auth instance
export const query = betterQuery({
plugins: [
auditPlugin({
enabled: true,
operations: ["create", "update", "delete"],
includeData: true,
})
],
resources: [
createResource({
name: "product",
schema: productSchema,
middlewares: [
{
handler: async (context) => {
// Extract user from Better Auth session
const session = await auth.api.getSession({
headers: context.request.headers,
});
if (session) context.user = session.user;
}
}
],
permissions: { /* ... */ }
})
]
})With this setup, audit logs will automatically include:
- User ID from the authenticated session
- User email and other user information
- IP address and user agent from the request