Creating Events

The SDK provides server actions that make it easy to create audit events from your Next.js server components, API routes, and server actions. These functions handle authentication, request formatting, and error handling for you.

Why Server Actions?

Server actions are perfect for creating audit events because:

  • Security: Your API key stays on the server, never exposed to the client
  • Simplicity: No need to manually format requests or handle authentication
  • Type safety: Full TypeScript support with proper types
  • Error handling: Built-in error handling and retry logic

Basic Usage

Creating a Single Event

import { createEvent } from '@archiva/archiva-nextjs';

// In a server component, API route, or server action
export async function updateInvoice(invoiceId: string, userId: string) {
  // Your business logic here...
  
  // Create an audit event
  const { eventId, replayed } = await createEvent({
    actionKey: 'invoice.update',
    actionDescription: `Invoice ${invoiceId} was updated`,
    entityType: 'invoice',
    entityId: invoiceId,
    actorType: 'user',
    actorId: userId,
    actorDisplay: 'John Doe',
    context: {
      changes: { status: 'draft' },
      requestId: crypto.randomUUID(),
    },
  });
  
  return { success: true, eventId };
}

Creating Multiple Events (Bulk)

import { createEvents } from '@archiva/archiva-nextjs';

// Create multiple events at once
const { eventIds } = await createEvents([
  {
    actionKey: 'invoice.create',
    entityType: 'invoice',
    entityId: 'inv_001',
    actorType: 'user',
    actorId: 'usr_123',
  },
  {
    actionKey: 'invoice.create',
    entityType: 'invoice',
    entityId: 'inv_002',
    actorType: 'user',
    actorId: 'usr_123',
  },
]);

Function Reference

createEvent

Creates a single audit event.

Signature:

createEvent(
  event: CreateEventInput,
  options?: CreateEventOptions,
  apiKey?: string
): Promise<{ eventId: string; replayed: boolean }>

Parameters:

ParameterTypeRequiredDescription
eventCreateEventInputYesThe event data to create
optionsCreateEventOptionsNoOptional idempotency key and request ID
apiKeystringNoOptional API key (defaults to ARCHIVA_SECRET_KEY env var)

Returns:

  • eventId: The unique identifier of the created event
  • replayed: true if this was an idempotent replay (same idempotency key and body), false if it's a new event

createEvents

Creates multiple audit events in a single request (bulk operation).

Signature:

createEvents(
  events: CreateEventInput[],
  options?: CreateEventOptions,
  apiKey?: string
): Promise<{ eventIds: string[] }>

Parameters:

ParameterTypeRequiredDescription
eventsCreateEventInput[]YesArray of events to create
optionsCreateEventOptionsNoOptional idempotency key and request ID (applied to all events)
apiKeystringNoOptional API key (defaults to ARCHIVA_SECRET_KEY env var)

Returns:

  • eventIds: Array of unique identifiers for the created events

Event Input Type

The CreateEventInput type defines what data you can include in an event:

type CreateEventInput = {
  // Action information
  actionKey?: string;           // e.g., 'invoice.update'
  actionDescription?: string;   // Human-readable description
  action?: string;              // Legacy field (use actionKey instead)
  
  // Entity information
  entityType: string;           // Required: e.g., 'invoice', 'user', 'order'
  entityId: string;            // Required: unique identifier
  
  // Actor information
  actorType?: 'user' | 'service' | 'system';  // Default: 'user'
  actorId: string;              // Required: unique identifier
  actorDisplay?: string;       // Human-readable name
  
  // Additional context
  tenantId?: string;            // For multi-tenant applications
  occurredAt?: string;          // ISO 8601 timestamp (defaults to now)
  source?: string;             // e.g., 'web-app', 'api', 'background-job'
  context?: Record<string, unknown>;  // Additional metadata (max 10KB)
  changes?: EventChange[];      // Field-level changes
};

Options Type

The CreateEventOptions type allows you to specify idempotency and request tracking:

type CreateEventOptions = {
  idempotencyKey?: string;     // For safe retries (see Idempotency docs)
  requestId?: string;           // For tracking and logging
};

Examples

In a Server Action

'use server';

import { createEvent } from '@archiva/archiva-nextjs';

export async function updateInvoiceStatus(
  invoiceId: string,
  newStatus: string,
  userId: string
) {
  // Update invoice in database
  await db.invoice.update({
    where: { id: invoiceId },
    data: { status: newStatus },
  });
  
  // Log the audit event
  await createEvent({
    actionKey: 'invoice.status.update',
    actionDescription: `Invoice status changed to ${newStatus}`,
    entityType: 'invoice',
    entityId: invoiceId,
    actorType: 'user',
    actorId: userId,
    changes: [
      {
        op: 'set',
        path: 'status',
        before: 'draft',
        after: newStatus,
      },
    ],
  });
  
  return { success: true };
}

In an API Route

import { createEvent } from '@archiva/archiva-nextjs';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const body = await request.json();
  
  // Your business logic...
  
  // Create audit event
  try {
    const { eventId } = await createEvent({
      actionKey: 'payment.processed',
      entityType: 'payment',
      entityId: body.paymentId,
      actorType: 'system',
      actorId: 'payment-service',
      context: {
        amount: body.amount,
        currency: body.currency,
      },
    }, {
      requestId: request.headers.get('x-request-id') || crypto.randomUUID(),
    });
    
    return NextResponse.json({ success: true, eventId });
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to create audit event' },
      { status: 500 }
    );
  }
}

With Idempotency

import { createEvent } from '@archiva/archiva-nextjs';

export async function processWebhook(webhookId: string, eventData: any) {
  // Use webhook ID as idempotency key to prevent duplicates
  const { eventId, replayed } = await createEvent({
    actionKey: 'webhook.processed',
    entityType: 'webhook',
    entityId: webhookId,
    actorType: 'service',
    actorId: 'webhook-handler',
    context: eventData,
  }, {
    idempotencyKey: `webhook_${webhookId}`,
  });
  
  if (replayed) {
    console.log('Event was already processed (idempotent replay)');
  }
  
  return { eventId };
}

Bulk Creation

import { createEvents } from '@archiva/archiva-nextjs';

export async function importHistoricalEvents(events: CreateEventInput[]) {
  // Create events in batches of 100
  const batchSize = 100;
  const batches = [];
  
  for (let i = 0; i < events.length; i += batchSize) {
    const batch = events.slice(i, i + batchSize);
    batches.push(createEvents(batch));
  }
  
  // Process all batches
  const results = await Promise.all(batches);
  const allEventIds = results.flatMap(r => r.eventIds);
  
  return { eventIds: allEventIds, count: allEventIds.length };
}

Error Handling

The functions throw ArchivaError for API errors:

import { createEvent, ArchivaError } from '@archiva/archiva-nextjs';

try {
  await createEvent({ /* ... */ });
} catch (error) {
  if (error instanceof ArchivaError) {
    switch (error.code) {
      case 'UNAUTHORIZED':
        // API key is invalid
        break;
      case 'RATE_LIMITED':
        // Rate limit exceeded, wait error.retryAfterSeconds
        break;
      case 'IDEMPOTENCY_CONFLICT':
        // Same idempotency key used with different body
        break;
      default:
        // Other error
    }
  }
  throw error;
}

Requirements

  • Must have ARCHIVA_SECRET_KEY environment variable set (or provide apiKey parameter)
  • Must be used in server-side code (server components, API routes, server actions)
  • Requires network access to https://api.archiva.app (or your configured API URL)

Best Practices

  1. Create events after successful operations - Only log events for actions that actually completed
  2. Use descriptive action keys - Follow a consistent format like entity.action (e.g., invoice.update)
  3. Include context - Add relevant metadata in the context field for debugging
  4. Use idempotency keys - For retryable operations (webhooks, scheduled jobs), use idempotency keys
  5. Handle errors gracefully - Don't let audit logging failures break your main functionality

Next Steps