pbaudit

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 22, 2026 License: MIT Imports: 5 Imported by: 1

README ΒΆ

PocketBase Audit Logging (pb-audit)

A comprehensive, production-ready audit logging library for PocketBase applications. Track all database operations, API requests, and authentication events with complete before/after state tracking.

Features

  • πŸ“ Dual-tracking system: Captures both user intent (requests) and actual results (commits)
  • πŸ”„ Complete change history: Before and after states for all operations
  • πŸ‘€ User attribution: Tracks who performed each action
  • 🌐 Request metadata: IP addresses, HTTP methods, URLs, and more
  • πŸ” Authentication events: Login tracking with auth method details
  • πŸ›‘οΈ Recursion prevention: Automatically skips logging on audit collection itself
  • πŸš€ Auto-setup: Creates collection and indexes automatically
  • βš™οΈ Non-destructive: Preserves your customizations after initial setup
  • 🎯 Flexible filtering: Optional custom logic to control what gets logged
  • 🧹 Retention policies: Automatic cleanup by age or record count on a cron schedule
  • πŸ“Š Optimized queries: Composite indexes for common query patterns

Installation

go get github.com/skeeeon/pb-audit

Quick Start

package main

import (
    "log"
    "github.com/pocketbase/pocketbase"
    "github.com/skeeeon/pb-audit"
)

func main() {
    app := pocketbase.New()
    
    // Setup audit logging with default options
    if err := pbaudit.Setup(app, pbaudit.DefaultOptions()); err != nil {
        log.Fatalf("Failed to setup audit logging: %v", err)
    }
    
    if err := app.Start(); err != nil {
        log.Fatal(err)
    }
}

Understanding the Dual-Tracking System

pb-audit uses a unique dual-tracking approach that provides complete visibility into operations:

Request Events (Before Commit)
  • Captured when API request is received
  • Include before state for updates/deletes
  • Include request context: IP, user, HTTP method, URL
  • May not complete if validation fails

Event Types: create_request, update_request, delete_request

Success Events (After Commit)
  • Captured when database operation succeeds
  • Confirm operation committed to database
  • Include final state after all hooks/validations
  • Guarantee operation completed

Event Types: create, update, delete

Admin/Superuser Operations

Important: When admins perform operations through the PocketBase Admin UI:

  • Success events are always logged (confirming database operations)
  • Request events are logged with user field as null (admins aren't in users collection)
  • Auth events for admin login are NOT logged (only regular user authentication)

This is by design - admins/superusers are stored separately from regular users and cannot be linked via the user relation field.

Why Both?

This dual approach answers different questions:

  • "What did the user try to do?" β†’ Request events
  • "What actually happened?" β†’ Success events
  • "Why did it fail?" β†’ Request event exists, no success event

Example timeline for updating a record:

  1. update_request - User submitted changes via API (before state captured)
  2. Validation runs
  3. Business logic hooks execute
  4. update - Database commit succeeded (after state captured)

Configuration Options

Customize audit logging behavior:

options := pbaudit.DefaultOptions()

// Custom collection name (default: "audit_logs")
options.CollectionName = "my_custom_audit_logs"

// Disable specific event types
options.LogAuthEvents = false      // Don't log authentication events
options.LogSuccessEvents = false   // Only log request events

// Custom event filtering
options.EventFilter = func(collectionName, eventType string) bool {
    // Only log events for sensitive collections
    if collectionName == "users" || collectionName == "payments" {
        return true
    }
    
    // Or filter by event type
    // return eventType == "delete" || eventType == "delete_request"
    
    return false
}

// Disable console logging
options.LogToConsole = false

// Automatic retention policy
options.Retention = &pbaudit.RetentionPolicy{
    MaxAge:     90 * 24 * time.Hour, // Delete logs older than 90 days
    MaxRecords: 100000,              // Keep at most 100k records
    Interval:   "0 2 * * *",        // Run cleanup at 2 AM daily
}

if err := pbaudit.Setup(app, options); err != nil {
    log.Fatal(err)
}

Retention Policy

pb-audit can automatically clean up old audit logs on a schedule using PocketBase's built-in cron scheduler. Configure a retention policy to keep your audit collection bounded without external scripts.

options := pbaudit.DefaultOptions()
options.Retention = &pbaudit.RetentionPolicy{
    MaxAge:     30 * 24 * time.Hour, // Delete records older than 30 days
    MaxRecords: 50000,               // Keep at most 50k records
    Interval:   "0 0 * * *",        // Run daily at midnight (default)
}
Field Type Default Description
MaxAge time.Duration 0 (disabled) Delete records older than this duration
MaxRecords int 0 (disabled) Keep at most this many records (oldest deleted first)
Interval string "0 0 * * *" Cron expression for cleanup schedule

Behavior:

  • Both constraints can be used independently or together β€” when both are set, both are enforced
  • If neither MaxAge nor MaxRecords is set, no cleanup job is registered
  • Deletion happens in batches to avoid excessive memory usage
  • Cleanup errors are logged but never block the application

Audit Logs Collection

The library automatically creates an audit_logs collection with these fields:

Field Type Description
event_type Select Type of operation (create, update, delete, etc.)
collection_name Text Collection where event occurred
record_id Text (optional) ID of the affected record (empty for create_request events)
user Relation β†’ users (optional) User who performed the action (null for admin/superuser actions)
auth_method Text Authentication method (for auth events)
request_method Text HTTP method (GET, POST, PUT, DELETE)
request_ip Text Client IP address
request_url Text URL path of the request
timestamp Date When the event occurred
before_changes JSON Record state before operation
after_changes JSON Record state after operation
created Date Auto-generated creation timestamp
updated Date Auto-generated update timestamp
Key Design Decisions

User Field is Optional:

  • Relation to users collection
  • null for admin/superuser actions (admins are not in users collection)
  • CascadeDelete: false - audit logs survive user deletion
  • Only set for regular user authentication

Record ID is Optional:

  • Empty for create_request events (record not yet saved)
  • Always present for success events (record committed with ID)

Before/After as JSON Fields:

  • Structured data instead of text strings
  • Efficient querying and parsing
  • 2MB size limit per field

Admin-Only Access (Default):

  • List, view, create, update, delete: admin only
  • Prevents users from tampering with audit logs
  • Can be customized after initial setup

Change Tracking Matrix

Event Type Before State After State Record ID User Request Metadata
create_request ❌ βœ… ❌ (not yet saved) βœ…* βœ… (IP, user, method, URL)
create ❌ βœ… βœ… ⚠️ ❌
update_request βœ… βœ… βœ… βœ…* βœ… (IP, user, method, URL)
update ❌ βœ… βœ… ⚠️ ❌
delete_request βœ… ❌ βœ… βœ…* βœ… (IP, user, method, URL)
delete βœ… ❌ βœ… ⚠️ ❌
auth ❌ βœ… βœ… βœ… βœ… (IP, method, auth_method)

Legend:

  • βœ… = Always present
  • ❌ = Not available
  • ⚠️ = May be null (not tracked for success events)
  • βœ…* = Present for regular users, null for admin/superuser operations

Usage Examples

Query Audit Logs via API
// JavaScript/TypeScript example

// Get recent audit logs
const logs = await pb.collection('audit_logs').getList(1, 50, {
    sort: '-timestamp'
});

// Find all changes to a specific record
const recordHistory = await pb.collection('audit_logs').getList(1, 100, {
    filter: 'record_id = "RECORD_ID"',
    sort: '-timestamp'
});

// Track user activity
const userActivity = await pb.collection('audit_logs').getList(1, 100, {
    filter: 'user = "USER_ID"',
    sort: '-timestamp',
    expand: 'user'
});

// Find all deletions
const deletions = await pb.collection('audit_logs').getList(1, 50, {
    filter: 'event_type = "delete" || event_type = "delete_request"',
    sort: '-timestamp'
});

// Filter by collection and date range
const recentUserChanges = await pb.collection('audit_logs').getList(1, 50, {
    filter: 'collection_name = "users" && timestamp >= "2024-01-01 00:00:00"',
    sort: '-timestamp'
});
Advanced Filtering
options := pbaudit.DefaultOptions()

// Example 1: Only log specific collections
options.EventFilter = func(collectionName, eventType string) bool {
    sensitiveCollections := []string{"users", "payments", "orders"}
    for _, col := range sensitiveCollections {
        if col == collectionName {
            return true
        }
    }
    return false
}

// Example 2: Only log destructive operations
options.EventFilter = func(collectionName, eventType string) bool {
    return eventType == "delete" || 
           eventType == "delete_request" || 
           eventType == "update" || 
           eventType == "update_request"
}

// Example 3: Skip temporary collections
options.EventFilter = func(collectionName, eventType string) bool {
    return !strings.HasPrefix(collectionName, "temp_")
}

if err := pbaudit.Setup(app, options); err != nil {
    log.Fatal(err)
}

IP Address Extraction

pb-audit handles complex proxy scenarios with intelligent IP extraction:

Priority Order:

  1. CF-Connecting-IP - Cloudflare (most reliable behind CDN)
  2. X-Forwarded-For - Standard proxy (takes first/original IP)
  3. X-Real-IP - Nginx and reverse proxies
  4. Fly-Client-IP - Fly.io platform

Security Note: X-Forwarded-For can be spoofed. In production behind a trusted reverse proxy, ensure your proxy is configured correctly.

Non-Destructive Setup

pb-audit follows a non-destructive philosophy:

βœ… First Setup:

  • Creates audit_logs collection
  • Sets default API rules (admin-only)
  • Creates indexes

βœ… Subsequent Starts:

  • Detects existing collection
  • Skips schema modifications
  • Preserves your custom API rules
  • Always registers hooks

This means you can:

  • Modify API rules without them being overwritten
  • Add custom fields to audit logs
  • Change indexes as needed
  • Update collection settings

The hooks always register, ensuring audit logging continues even if the collection was modified.

Performance Considerations

Indexes

The collection includes optimized indexes for common queries:

-- Single column indexes
CREATE INDEX idx_audit_collection_name ON audit_logs (collection_name)
CREATE INDEX idx_audit_record_id ON audit_logs (record_id)
CREATE INDEX idx_audit_timestamp ON audit_logs (timestamp)
CREATE INDEX idx_audit_user ON audit_logs (user)
CREATE INDEX idx_audit_event_type ON audit_logs (event_type)

-- Composite indexes for common patterns
CREATE INDEX idx_audit_collection_timestamp ON audit_logs (collection_name, timestamp)
CREATE INDEX idx_audit_user_timestamp ON audit_logs (user, timestamp)
Error Handling

Audit logging failures never block your application:

  • Errors are logged to console (if enabled)
  • Operations continue normally
  • This ensures audit logging doesn't impact user experience
Storage Considerations
  • Each audit log can store up to 2MB of data per state field
  • Consider implementing cleanup for old logs
  • Archive or delete logs based on your retention policy

Maintenance

Cleaning Old Logs

The recommended approach is to use the built-in Retention Policy:

options.Retention = &pbaudit.RetentionPolicy{
    MaxAge: 180 * 24 * time.Hour, // 6 months
}

Alternatively, you can clean up manually via the API:

// JavaScript example - run periodically (cron, etc.)
const sixMonthsAgo = new Date();
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);

const oldLogs = await pb.collection('audit_logs').getFullList({
    filter: `timestamp < "${sixMonthsAgo.toISOString()}"`
});

for (const log of oldLogs) {
    await pb.collection('audit_logs').delete(log.id);
}
Tracking Admin Operations

If you need to identify which admin performed an operation, you have a few options:

Option 1: Check request_ip field Admins operations will have null user but will have request_ip populated for request events.

Option 2: Add a custom admin tracking field After initial setup, you can add a text field to track admin ID:

// In PocketBase Admin UI β†’ Collections β†’ audit_logs:
// Add field: name="admin_email", type="text"

Then modify your audit hook to capture admin info (requires custom PocketBase setup).

Option 3: Filter by null user

// Find all operations by admins (user is null)
const adminOps = await pb.collection('audit_logs').getList(1, 50, {
    filter: 'user = null',
    sort: '-timestamp'
});
Custom API Rules

After setup, you can modify API rules for your needs:

// Example: Allow users to view their own audit logs
// In PocketBase Admin UI β†’ Collections β†’ audit_logs β†’ API Rules:

// List Rule:
// @request.auth.type = 'admin' || user.id = @request.auth.id

// View Rule:
// @request.auth.type = 'admin' || user.id = @request.auth.id

Contributing

Contributions are welcome! Please follow the "grug brained developer" philosophy:

  • Simple, explicit code
  • Clear documentation
  • One file, one purpose
  • Comprehensive comments

License

MIT License - see LICENSE file for details.

Version

Current version: 2.0.0

Changes from 1.x:

  • Restructured to internal/audit/ package
  • Changed user_id from TextField to RelationField
  • Changed before/after from TextField to JSONField
  • Non-destructive setup (preserves customizations)
  • Improved documentation
  • Better hook organization
  • Cleaner IP extraction

Documentation ΒΆ

Overview ΒΆ

Package pbaudit provides comprehensive audit logging for PocketBase applications.

This library tracks all record operations (create, update, delete), API requests, and authentication events, creating a complete audit trail with before/after states, user attribution, and request metadata.

DUAL-TRACKING SYSTEM: pb-audit uses a dual-tracking approach for complete audit trails:

1. REQUEST EVENTS (before commit):

  • Capture user intent and request context (IP, user, method, URL)
  • Include before state for updates/deletes
  • Fire before database commit (may not complete if validation fails)

2. SUCCESS EVENTS (after commit):

  • Confirm operation committed to database
  • Include final state after all hooks/validations
  • Guarantee operation succeeded

This provides visibility into both what was attempted and what actually happened.

Example usage:

app := pocketbase.New()
if err := pbaudit.Setup(app, pbaudit.DefaultOptions()); err != nil {
    log.Fatalf("Failed to setup audit logging: %v", err)
}
app.Start()

Index ΒΆ

Constants ΒΆ

View Source
const Version = "2.0.0"

Version is the library version.

Variables ΒΆ

This section is empty.

Functions ΒΆ

func Setup ΒΆ

func Setup(app *pocketbase.PocketBase, options Options) error

Setup initializes audit logging for a PocketBase application.

This is the main entry point that creates the audit collection and registers hooks.

BEHAVIOR: - Non-destructive: Only creates collection if it doesn't exist - Preserves customizations: Won't overwrite API rules after initial setup - Always registers hooks: Even if collection already exists

PARAMETERS:

  • app: PocketBase application instance
  • options: Configuration options (use DefaultOptions() for defaults)

RETURNS:

  • nil on successful setup
  • error if setup fails

Example:

app := pocketbase.New()

// With default options
if err := pbaudit.Setup(app, pbaudit.DefaultOptions()); err != nil {
    log.Fatal(err)
}

// With custom options
options := pbaudit.DefaultOptions()
options.CollectionName = "my_audit_logs"
options.LogAuthEvents = false
if err := pbaudit.Setup(app, options); err != nil {
    log.Fatal(err)
}

Types ΒΆ

type Options ΒΆ

type Options struct {
	// Collection configuration
	CollectionName string // Name for audit logs collection (default: "audit_logs")

	// What to log
	LogRequestEvents bool // Log API request events (default: true)
	LogSuccessEvents bool // Log database success events (default: true)
	LogAuthEvents    bool // Log authentication events (default: true)

	// Optional filtering
	// EventFilter allows custom filtering logic for events
	// Return true to log the event, false to skip it
	// Parameters: collectionName, eventType
	//
	// Example:
	//   EventFilter: func(collectionName, eventType string) bool {
	//       // Only log events for sensitive collections
	//       return collectionName == "users" || collectionName == "payments"
	//   }
	EventFilter func(collectionName, eventType string) bool

	// Retention policy for automatic cleanup (nil = no cleanup)
	Retention *RetentionPolicy

	// Logging
	LogToConsole bool // Enable console logging (default: true)
}

Options configures the behavior of audit logging.

func DefaultOptions ΒΆ

func DefaultOptions() Options

DefaultOptions returns sensible defaults for audit logging.

Default configuration:

  • CollectionName: "audit_logs"
  • LogRequestEvents: true (track API operations)
  • LogSuccessEvents: true (track database operations)
  • LogAuthEvents: true (track authentication)
  • EventFilter: nil (log all events)
  • LogToConsole: true (enable logging)

type RetentionPolicy ΒΆ

type RetentionPolicy struct {
	MaxAge     time.Duration // Delete records older than this duration (0 = disabled)
	MaxRecords int           // Keep at most this many records, oldest deleted first (0 = disabled)
	Interval   string        // Cron expression for cleanup schedule (default: "0 0 * * *" = daily midnight)
}

RetentionPolicy configures automatic cleanup of old audit logs.

Both MaxAge and MaxRecords can be used independently or together. When both are set, both constraints are enforced (records must satisfy both). If neither is set, no cleanup is performed.

Example:

options.Retention = &pbaudit.RetentionPolicy{
    MaxAge:     90 * 24 * time.Hour, // Delete logs older than 90 days
    MaxRecords: 100000,              // Keep at most 100k records
    Interval:   "0 2 * * *",         // Run cleanup at 2 AM daily
}

Directories ΒΆ

Path Synopsis
examples
basic command
internal
audit
Package audit implements comprehensive audit logging for PocketBase applications.
Package audit implements comprehensive audit logging for PocketBase applications.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL