TRAILBASE|DOCUMENTATION

Audit User Actions

Learn how to comprehensively track user activity in your application, from authentication to data modifications.

Overview

Auditing user actions is critical for security, compliance, and debugging. This guide shows you how to instrument your application to capture meaningful audit events without impacting performance or user experience.

What to Audit

Not every action needs to be logged. Focus on security-relevant and business-critical operations:

Authentication Events

  • Login attempts (success and failure)
  • Logout actions
  • Password changes and resets
  • MFA enrollment and verification
  • Session creation and revocation

Authorization Events

  • Permission grants and revocations
  • Role assignments
  • Access denials (especially important for compliance)
  • Resource sharing actions

Data Modification Events

  • Create, update, delete operations on critical resources
  • Bulk operations (e.g., batch delete)
  • Data exports and downloads
  • Configuration changes

Administrative Actions

  • User account creation, suspension, deletion
  • Billing and subscription changes
  • API key generation and revocation
  • System configuration updates

Implementation Patterns

1. Middleware Pattern (Recommended)

For web applications, use middleware to automatically audit HTTP requests:

// middleware/audit.ts
import { TrailbaseClient } from '@trailbase/sdk';
import { NextRequest, NextResponse } from 'next/server';

const trailbase = new TrailbaseClient({
  apiKey: process.env.TRAILBASE_API_KEY!,
  tenantId: process.env.TRAILBASE_TENANT_ID!,
});

export async function auditMiddleware(
  req: NextRequest,
  context: { user: { id: string; email: string } }
) {
  const startTime = Date.now();
  const response = await NextResponse.next();
  const duration = Date.now() - startTime;

  // Only audit state-changing operations
  if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
    await trailbase.log({
      action: `api.${req.method.toLowerCase()}`,
      actor: {
        id: context.user.id,
        email: context.user.email,
        ip: req.headers.get('x-forwarded-for') || req.ip || 'unknown',
        user_agent: req.headers.get('user-agent') || 'unknown',
      },
      resource: {
        type: 'api_endpoint',
        id: req.nextUrl.pathname,
      },
      outcome: response.status < 400 ? 'success' : 'failure',
      metadata: {
        method: req.method,
        path: req.nextUrl.pathname,
        status_code: response.status,
        duration_ms: duration,
      },
    });
  }

  return response;
}

2. Service Layer Pattern

For complex business logic, audit at the service layer for maximum context:

// services/document.service.ts
import { trailbase } from '@/lib/trailbase';

export class DocumentService {
  async shareDocument(
    documentId: string,
    actorId: string,
    actorEmail: string,
    recipientEmail: string,
    permission: 'read' | 'write'
  ) {
    try {
      // Perform the share operation
      await db.documentShare.create({
        data: {
          documentId,
          recipientEmail,
          permission,
        },
      });

      // Audit the successful share
      await trailbase.log({
        action: 'document.share',
        actor: {
          id: actorId,
          email: actorEmail,
        },
        resource: {
          type: 'document',
          id: documentId,
        },
        outcome: 'success',
        metadata: {
          shared_with: recipientEmail,
          permission,
          expires_at: null,
        },
      });

      return { success: true };
    } catch (error) {
      // Audit the failure
      await trailbase.log({
        action: 'document.share',
        actor: {
          id: actorId,
          email: actorEmail,
        },
        resource: {
          type: 'document',
          id: documentId,
        },
        outcome: 'failure',
        metadata: {
          error: error.message,
          shared_with: recipientEmail,
        },
      });

      throw error;
    }
  }
}

3. Decorator Pattern

Use TypeScript decorators for clean, declarative auditing:

// decorators/audit.ts
import { trailbase } from '@/lib/trailbase';

export function Audit(action: string, resourceType: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      const context = args[0]; // Assume first arg contains user context

      try {
        const result = await originalMethod.apply(this, args);

        await trailbase.log({
          action,
          actor: {
            id: context.userId,
            email: context.userEmail,
          },
          resource: {
            type: resourceType,
            id: result.id || 'unknown',
          },
          outcome: 'success',
          metadata: { result },
        });

        return result;
      } catch (error) {
        await trailbase.log({
          action,
          actor: {
            id: context.userId,
            email: context.userEmail,
          },
          resource: {
            type: resourceType,
            id: 'unknown',
          },
          outcome: 'failure',
          metadata: { error: error.message },
        });

        throw error;
      }
    };

    return descriptor;
  };
}

// Usage:
class UserService {
  @Audit('user.delete', 'user')
  async deleteUser(context: Context, userId: string) {
    // Implementation
  }
}

Metadata Best Practices

The metadata field is your opportunity to add context. Here's what to include:

Changed Fields (for Updates)

await trailbase.log({
  action: 'profile.update',
  actor: { id: userId, email: userEmail },
  resource: { type: 'user', id: userId },
  outcome: 'success',
  metadata: {
    changed_fields: ['email', 'phone'],
    old_values: {
      email: 'old@example.com',
      phone: '+1234567890',
    },
    new_values: {
      email: 'new@example.com',
      phone: '+0987654321',
    },
  },
});

Request Context

metadata: {
  request_id: req.headers.get('x-request-id'),
  session_id: req.cookies.get('session_id'),
  referrer: req.headers.get('referer'),
  country: geoip.lookup(ip)?.country,
}

Business Context

metadata: {
  tenant_plan: 'enterprise',
  feature_flag: 'new_editor_enabled',
  experiment_variant: 'control',
  impersonated_by: adminId, // If admin is acting as user
}

Handling Sensitive Data

Never log passwords, API keys, or personally identifiable information (PII) unless required:

// ❌ BAD: Logging password
await trailbase.log({
  action: 'user.login',
  metadata: {
    password: userPassword, // NEVER DO THIS
  },
});

// ✅ GOOD: Redact sensitive data
await trailbase.log({
  action: 'user.login',
  metadata: {
    password_length: userPassword.length,
    password_strength: 'strong',
  },
});

GDPR Compliance

If you're subject to GDPR, avoid logging email addresses or names in metadata. Use pseudonymous user IDs instead and maintain a separate mapping table that can be deleted upon user request.

Performance Optimization

1. Use Automatic Batching

The SDK batches events automatically, but you can tune the settings:

const trailbase = new TrailbaseClient({
  apiKey: process.env.TRAILBASE_API_KEY!,
  tenantId: process.env.TRAILBASE_TENANT_ID!,
  flushInterval: 500,    // Flush every 500ms (default: 1000)
  maxBatchSize: 100,     // Max events per batch (default: 50)
});

2. Fire-and-Forget Pattern

Don't await audit logging in critical paths:

// ❌ BAD: Blocks user response
async function updateDocument(data) {
  const result = await db.update(data);
  await trailbase.log({ ... }); // User waits for this
  return result;
}

// ✅ GOOD: Non-blocking
async function updateDocument(data) {
  const result = await db.update(data);
  trailbase.log({ ... }); // Fire and forget (queued for batching)
  return result;
}

3. Background Worker Pattern

For extremely high-volume scenarios, use a message queue:

// In your API handler
await queue.publish('audit', {
  action: 'document.create',
  actor: { id: userId, email: userEmail },
  resource: { type: 'document', id: docId },
  outcome: 'success',
});

// In a separate worker process
queue.subscribe('audit', async (event) => {
  await trailbase.log(event);
});

Testing Your Implementation

Unit Testing

Mock the Trailbase client in your tests:

// __tests__/document.service.test.ts
import { jest } from '@jest/globals';
import { DocumentService } from './document.service';

jest.mock('@/lib/trailbase', () => ({
  trailbase: {
    log: jest.fn(),
  },
}));

describe('DocumentService', () => {
  it('should audit successful share', async () => {
    const service = new DocumentService();

    await service.shareDocument(
      'doc_123',
      'user_alice',
      'alice@example.com',
      'bob@example.com',
      'read'
    );

    expect(trailbase.log).toHaveBeenCalledWith(
      expect.objectContaining({
        action: 'document.share',
        outcome: 'success',
      })
    );
  });
});

Integration Testing

Test against the Trailbase API in staging:

// e2e/audit.test.ts
import { TrailbaseClient } from '@trailbase/sdk';

const trailbase = new TrailbaseClient({
  apiKey: process.env.TRAILBASE_TEST_API_KEY!,
  tenantId: 'test_tenant',
});

test('end-to-end audit logging', async () => {
  const eventId = `test_${Date.now()}`;

  await trailbase.log({
    action: 'test.event',
    actor: { id: 'test_user', email: 'test@example.com' },
    resource: { type: 'test', id: eventId },
    outcome: 'success',
  });

  // Wait for ingestion
  await new Promise((r) => setTimeout(r, 2000));

  // Verify via search API
  const results = await trailbase.searchEvents({
    resource_id: eventId,
  });

  expect(results.events).toHaveLength(1);
  expect(results.events[0].action).toBe('test.event');
});

Monitoring and Alerts

Set up alerts to monitor audit log health:

// Create alert for failed login attempts
await trailbase.createAlertRule({
  name: 'Excessive Failed Logins',
  type: 'threshold',
  condition: {
    action: 'user.login',
    outcome: 'failure',
    count: 5,
    window_seconds: 300, // 5 minutes
  },
  severity: 'high',
  webhookIds: ['webhook_slack'],
});

Next Steps

Now that you're auditing user actions, learn how to control access with RBAC implementation.

Edit this page on GitHub