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.

query.ts
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 migrate
npx better-query generate

See 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.

query-client.ts
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.

GET
/audit/logs
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,});
PropDescriptionType
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:

audit-logs.tsx
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.

GET
/audit/resource/:resourceId
const { data: resourceHistory, error } = await authClient.audit.resource.:resourceid({    resourceId: "product-456", // required    resourceType: "product", // required    includeData: true,});
PropDescriptionType
resourceId
The ID of the resource
string
resourceType
The type of resource
string
includeData?
Include the actual data changes
boolean

Client Usage:

resource-history.tsx
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:

audit-stats.tsx
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:

query.ts
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:

query.ts
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:

hooks/use-audit.ts
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:

components/audit-log-viewer.tsx
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:

query.ts
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

On this page