Skip to main content
Studio can track and store authentication events like user signups, logins, password changes, and organization activities. This provides audit trails and insights into user behavior.

Overview

Event ingestion allows you to:
  • Track all authentication events (40+ event types)
  • Store events in various databases (Postgres, ClickHouse, SQLite, etc.)
  • Filter and monitor specific event types
  • Display events in a live marquee
  • Create custom event handlers
  • Query event history and statistics

Configuration

Configure events in your studio.config.ts:
studio.config.ts
import type { StudioConfig } from "better-auth-studio";
import { auth } from "./lib/auth";

const config: StudioConfig = {
  auth,
  basePath: "/api/studio",
  events: {
    enabled: true,
    tableName: "auth_events",
    clientType: "postgres",
    include: ["user.joined", "user.logged_in", "session.created"],
    batchSize: 100,
    flushInterval: 5000,
  },
};

export default config;

Event Options

events.enabled
boolean
Enable or disable event trackingDefault: false
events.tableName
string
Name of the table/collection to store eventsDefault: Uses Better Auth adapter’s default table
events.provider
EventIngestionProvider
Custom event ingestion provider implementation. Use this to implement your own event storage logic.
interface EventIngestionProvider {
  ingest(event: AuthEvent): Promise<void>;
  ingestBatch?(events: AuthEvent[]): Promise<void>;
  query?(options: EventQueryOptions): Promise<EventQueryResult>;
  count?(): Promise<number>;
  getStats?(): Promise<EventStats>;
  healthCheck?(): Promise<boolean>;
  shutdown?(): Promise<void>;
}
events.client
any
Client instance for the database (Prisma client, Drizzle instance, Postgres pool, ClickHouse client, etc.)
events.clientType
string
Type of database client being usedOptions: "postgres", "prisma", "drizzle", "clickhouse", "https", "custom", "sqlite", "node-sqlite"
events.include
AuthEventType[]
Array of event types to track. Only these events will be ingested.
include: [
  "user.joined",
  "user.logged_in",
  "session.created",
  "organization.created"
]
events.exclude
AuthEventType[]
Array of event types to exclude from tracking. All other events will be ingested.
exclude: [
  "user.logged_out",
  "session.created"
]
events.batchSize
number
Number of events to batch before flushing to the databaseDefault: 100
events.flushInterval
number
Interval in milliseconds to flush batched eventsDefault: 5000 (5 seconds)
events.retryOnError
boolean
Retry failed event ingestionDefault: false
events.onEventIngest
(event: AuthEvent) => void | Promise<void>
Callback function invoked when an event is ingested. Use this for custom processing, logging, or side effects.
onEventIngest: async (event) => {
  console.log('Event ingested:', event.type);
  // Send to analytics service
  // Trigger webhooks
  // Update metrics
}
events.liveMarquee
LiveMarqueeConfig
Configuration for the live event marquee displayed in StudioSee Live Marquee Configuration below

Event Types

Studio tracks 40+ authentication event types:

User Events

  • user.joined - User signed up
  • user.logged_in - User logged in
  • user.logged_out - User logged out
  • user.updated - User profile updated
  • user.password_changed - Password changed
  • user.email_verified - Email verified
  • user.banned - User banned
  • user.unbanned - User unbanned
  • user.deleted - User deleted
  • user.delete_verification_requested - Delete verification requested

Session Events

  • session.created - New session created
  • login.failed - Login attempt failed

Organization Events

  • organization.created - Organization created
  • organization.updated - Organization updated
  • organization.deleted - Organization deleted
  • member.added - Member added to organization
  • member.removed - Member removed from organization
  • member.role_changed - Member role changed

Team Events

  • team.created - Team created
  • team.updated - Team updated
  • team.deleted - Team deleted
  • team.member.added - Member added to team
  • team.member.removed - Member removed from team

OAuth Events

  • oauth.linked - OAuth account linked
  • oauth.unlinked - OAuth account unlinked
  • oauth.sign_in - User signed in via OAuth

Password Reset Events

  • password.reset_requested - Password reset requested
  • password.reset_completed - Password reset completed
  • password.reset_requested_otp - OTP password reset requested
  • password.reset_completed_otp - OTP password reset completed

Invitation Events

  • invitation.created - Invitation created
  • invitation.accepted - Invitation accepted
  • invitation.rejected - Invitation rejected
  • invitation.cancelled - Invitation cancelled

Phone Number Events

  • phone_number.otp_requested - Phone OTP requested
  • phone_number.verification - Phone number verified

Event Structure

interface AuthEvent {
  id: string;
  type: AuthEventType;
  timestamp: Date;
  status: "success" | "failed";
  userId?: string;
  sessionId?: string;
  organizationId?: string;
  metadata?: Record<string, any>;
  ipAddress?: string;
  userAgent?: string;
  source: "app" | "api";
  display?: {
    message: string;
    severity?: "info" | "success" | "warning" | "failed";
  };
}

Live Marquee Configuration

The live marquee displays recent events in real-time at the top of the Studio dashboard:
liveMarquee: {
  enabled: true,
  pollInterval: 2000, // 2 seconds
  speed: 0.5,
  pauseOnHover: true,
  limit: 50,
  sort: "desc", // newest first
  colors: {
    success: "#10b981",
    info: "#3b82f6",
    warning: "#f59e0b",
    error: "#ef4444",
    failed: "#dc2626",
  },
  timeWindow: {
    since: "1h" // Last 1 hour
  }
}
liveMarquee.enabled
boolean
Enable or disable the live event marqueeDefault: true
liveMarquee.pollInterval
number
Polling interval in milliseconds for fetching new eventsDefault: 2000 (2 seconds)
liveMarquee.speed
number
Animation speed in pixels per frameDefault: 0.5
liveMarquee.pauseOnHover
boolean
Pause animation when mouse hovers over the marqueeDefault: true
liveMarquee.limit
number
Maximum number of events to display in the marqueeDefault: 50
liveMarquee.sort
'asc' | 'desc'
Sort order for events
  • "desc": Newest first (default)
  • "asc": Oldest first
liveMarquee.colors
EventColors
Custom colors for different event severities
colors: {
  success: "#10b981",
  info: "#3b82f6",
  warning: "#f59e0b",
  error: "#ef4444",
  failed: "#dc2626",
}
liveMarquee.timeWindow
TimeWindowConfig
Time window for fetching events
// Predefined preset
timeWindow: { since: "1h" }

// Custom duration in seconds
timeWindow: { custom: 7200 } // 2 hours
Presets: "15m", "30m", "1h", "2h", "4h", "6h", "12h", "1d", "2d", "3d", "7d", "14d", "30d"

Example Configurations

Track All User Events

studio.config.ts
const config: StudioConfig = {
  auth,
  basePath: "/api/studio",
  events: {
    enabled: true,
    include: [
      "user.joined",
      "user.logged_in",
      "user.logged_out",
      "user.updated",
      "user.password_changed",
      "user.email_verified",
    ],
  },
};

Exclude Noisy Events

studio.config.ts
const config: StudioConfig = {
  auth,
  basePath: "/api/studio",
  events: {
    enabled: true,
    exclude: [
      "session.created", // Too frequent
      "user.logged_out", // Not important for audit
    ],
  },
};

Custom Event Handler

studio.config.ts
const config: StudioConfig = {
  auth,
  basePath: "/api/studio",
  events: {
    enabled: true,
    onEventIngest: async (event) => {
      // Send critical events to Slack
      if (event.type === "user.banned" || event.status === "failed") {
        await sendSlackNotification({
          channel: "#security-alerts",
          message: `Event: ${event.type} - Status: ${event.status}`,
          metadata: event.metadata,
        });
      }
      
      // Log all events
      console.log(`[${event.timestamp}] ${event.type}:`, event.status);
    },
  },
};

ClickHouse Integration

studio.config.ts
import { createClient } from '@clickhouse/client';

const clickhouse = createClient({
  host: process.env.CLICKHOUSE_HOST,
});

const config: StudioConfig = {
  auth,
  basePath: "/api/studio",
  events: {
    enabled: true,
    client: clickhouse,
    clientType: "clickhouse",
    tableName: "auth_events",
    batchSize: 1000,
    flushInterval: 10000, // 10 seconds
  },
};

TypeScript Types

type StudioConfig = {
  // ... other config
  events?: {
    enabled?: boolean;
    tableName?: string;
    provider?: EventIngestionProvider;
    client?: any;
    clientType?: "postgres" | "prisma" | "drizzle" | "clickhouse" | "https" | "custom" | "sqlite" | "node-sqlite";
    include?: AuthEventType[];
    exclude?: AuthEventType[];
    batchSize?: number;
    flushInterval?: number;
    retryOnError?: boolean;
    liveMarquee?: LiveMarqueeConfig;
    onEventIngest?: (event: AuthEvent) => void | Promise<void>;
  };
};

type LiveMarqueeConfig = {
  enabled?: boolean;
  pollInterval?: number;
  speed?: number;
  pauseOnHover?: boolean;
  limit?: number;
  sort?: "asc" | "desc";
  colors?: EventColors;
  timeWindow?: TimeWindowConfig;
};

type TimeWindowConfig =
  | { since: TimeWindowPreset; custom?: never }
  | { custom: number; since?: never };

type TimeWindowPreset =
  | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "12h"
  | "1d" | "2d" | "3d" | "7d" | "14d" | "30d";

Performance Considerations

High-Volume Applications:
  • Use batching (batchSize) to reduce database writes
  • Increase flushInterval for better batching efficiency
  • Use ClickHouse or other time-series databases for millions of events
  • Consider excluding high-frequency events like session.created
  • Implement event retention policies to manage database size
Small (< 1,000 events/day):
batchSize: 50,
flushInterval: 5000,
Medium (1,000 - 100,000 events/day):
batchSize: 500,
flushInterval: 10000,
Large (> 100,000 events/day):
batchSize: 2000,
flushInterval: 30000,
clientType: "clickhouse", // Use time-series DB

Build docs developers (and LLMs) love