Skip to main content
BunShip records every significant action in an append-only audit log. Each entry captures who did what, when, and what changed — providing a full trail for debugging, compliance, and security reviews.

What Gets Logged

Every audit log entry contains these fields:
FieldTypeDescription
idstringUnique log entry ID
organizationIdstringOrganization where the action occurred
actorIdstringID of the user, API key, or system process
actorType"user" | "api_key" | "system"What performed the action
actorEmailstringEmail of the user (if actorType is user)
actionstringAction identifier (e.g., organization.updated)
resourceTypestringType of resource affected (e.g., organization, member)
resourceIdstringID of the affected resource
oldValuesobjectPrevious state of changed fields
newValuesobjectNew state of changed fields
ipAddressstringClient IP address
userAgentstringClient user agent string
metadataobjectAdditional context
createdAttimestampWhen the action occurred

Example entry

{
  "id": "log_abc123",
  "organizationId": "org_xyz789",
  "actorId": "user_123",
  "actorType": "user",
  "actorEmail": "[email protected]",
  "action": "organization.updated",
  "resourceType": "organization",
  "resourceId": "org_xyz789",
  "oldValues": { "name": "Acme Corp" },
  "newValues": { "name": "Acme Inc" },
  "ipAddress": "203.0.113.42",
  "userAgent": "Mozilla/5.0 ...",
  "metadata": null,
  "createdAt": "2025-03-15T14:22:00.000Z"
}

Creating Audit Log Entries

The audit service provides typed helper methods for different actor types.

User actions

await auditService.logUserAction({
  organizationId: "org_123",
  userId: "user_123",
  userEmail: "[email protected]",
  action: "organization.updated",
  resourceType: "organization",
  resourceId: "org_123",
  oldValues: { name: "Old Name" },
  newValues: { name: "New Name" },
  ipAddress: "203.0.113.42",
  userAgent: "Mozilla/5.0 ...",
});

API key actions

await auditService.logApiKeyAction({
  organizationId: "org_123",
  apiKeyId: "key_456",
  action: "project.created",
  resourceType: "project",
  resourceId: "proj_789",
  ipAddress: "203.0.113.42",
});

System actions

await auditService.logSystemAction({
  organizationId: "org_123",
  action: "subscription.renewed",
  resourceType: "subscription",
  resourceId: "sub_abc",
  metadata: { planId: "pro", period: "monthly" },
});

Querying Audit Logs

List audit logs with filters, pagination, and date ranges:
curl "https://api.example.com/api/v1/audit-logs?action=organization.updated&limit=50&offset=0" \
  -H "Authorization: Bearer <token>"

Available filters

ParameterTypeDescription
actorIdstringFilter by actor ID
actorTypestringFilter by actor type (user, api_key, system)
actionstringFilter by action (e.g., member.added)
resourceTypestringFilter by resource type (e.g., organization)
resourceIdstringFilter by specific resource
startDateISO dateEntries after this date
endDateISO dateEntries before this date
limitnumberResults per page (default: 50)
offsetnumberPagination offset (default: 0)

Response format

{
  "logs": [
    {
      "id": "log_abc123",
      "actorType": "user",
      "actorEmail": "[email protected]",
      "action": "organization.updated",
      "resourceType": "organization",
      "resourceId": "org_xyz789",
      "createdAt": "2025-03-15T14:22:00.000Z"
    }
  ],
  "total": 142,
  "limit": 50,
  "offset": 0
}
The total field returns the full count of matching entries, so your frontend can calculate page numbers.

Filtering by date range

const { logs, total } = await auditService.list("org_123", {
  actorType: "user",
  action: "organization.updated",
  startDate: new Date("2025-01-01"),
  endDate: new Date("2025-03-31"),
  limit: 50,
  offset: 0,
});

Log Retention

Audit logs are cleaned up automatically by the background jobs system. Retention periods depend on your plan:
PlanRetention
FreeNot available
Pro30 days
Enterprise1 year
The cleanup worker runs weekly and removes entries older than the configured threshold:
await addCleanupJob(
  {
    task: "old-audit-logs",
    daysToKeep: 90,
  },
  {
    repeat: {
      cron: "0 3 * * 0", // Weekly on Sunday at 3 AM
    },
    jobId: "cleanup-old-audit-logs",
  }
);
Adjust the daysToKeep value in apps/api/src/jobs/index.ts to change the retention period.

Integration with Other Features

Audit logs are created automatically throughout BunShip. Here are the key integration points:

Authentication

Login attempts, password changes, 2FA setup, and session revocations are logged with the user’s IP address and user agent.

Team Management

Member invitations, role changes, and removals record both the actor and the affected member.

Billing

Subscription changes, checkout completions, and cancellations are logged as system actions triggered by Stripe webhooks.

API Keys

Key creation and revocation are logged. Requests authenticated with API keys use actorType: "api_key" for attribution.

Adding audit logs to custom routes

When building new features, add audit logging at the service layer:
import { auditService } from "../services/audit.service";

// After performing the action
await auditService.logUserAction({
  organizationId: org.id,
  userId: user.id,
  userEmail: user.email,
  action: "custom_resource.created",
  resourceType: "custom_resource",
  resourceId: newResource.id,
  newValues: { name: newResource.name },
  ipAddress: request.ip,
  userAgent: request.headers.get("user-agent") ?? undefined,
});
The audit service catches and logs its own errors internally, so a failure to write an audit entry will not break the primary operation. Errors are printed to stderr and an InternalError is thrown, which the global error handler catches.