SELF-HOSTING

Deploy Better Auth Studio on your own infrastructure.

Beta Feature

Self-hosting is currently in beta. This feature allows you to deploy Better Auth Studio alongside your application for production use. You may encounter bugs or incomplete features. Please report any issues on GitHub.

Overview

Self-hosting Better Auth Studio allows you to embed the admin dashboard directly into your application. This enables you to access the studio at a custom route like /api/studio or /admin.

Benefits include:

  • Deploy studio alongside your app in production
  • Role-based access control with admin login
  • Restrict access to specific admin emails
  • Framework-agnostic (Next.js, Express, and more)

Prerequisites

  • 1.Better Auth Studio installed as a regular dependency (required for production)
  • 2.A Better Auth project with valid auth.ts configuration
  • 3.Database adapter configured (Prisma, Drizzle, or SQLite)

⚠️ Important: For self-hosting, install as a regular dependency (not devDependency) since it's needed at runtime in production.

pnpm add better-auth-studio

Step 1: Initialize Studio Config

Run the init command to generate the configuration file:

pnpx better-auth-studio init

This creates a studio.config.ts file in your project root:

import type { StudioConfig } from "better-auth-studio";
import { auth } from "./lib/auth";

const config: StudioConfig = {
  auth,
  basePath: "/api/studio",
  metadata: {
    title: "Admin Dashboard",
    theme: "dark",
  },
  access: {
    roles: ["admin"],
    allowEmails: ["admin@example.com"],
  },
};

export default config;

For Next.js App Router, the init command automatically creates the API route file at app/api/studio/[[...path]]/route.ts:

import { betterAuthStudio } from "better-auth-studio/nextjs";
import studioConfig from "@/studio.config";

const handler = betterAuthStudio(studioConfig);

export {
  handler as GET,
  handler as POST,
  handler as PUT,
  handler as DELETE,
  handler as PATCH,
};

Access the studio at /api/studio

Configuration Options

auth(required)

Your Better Auth instance from auth.ts

basePath(required)

The URL path where studio is mounted (e.g., /api/studio)

⚠️ Important: When adjusting the basePath, make sure to adjust your route structure accordingly when mounting the handler.

For example, if your basePath is /admin, your route file should be at app/admin/[[...path]]/route.ts to matching the path structure.

access.allowEmails(optional)

Array of email addresses allowed to access the studio

💡 Best Practice: Use environment variables for configuration to keep sensitive data out of your codebase:

// studio.config.ts
import type { StudioConfig } from "better-auth-studio";
import { auth } from "./lib/auth";

const config: StudioConfig = {
  auth,
  basePath: process.env.STUDIO_BASE_PATH || "/api/studio",
  access: {
    allowEmails: [
      process.env.ADMIN_EMAIL_1,
      process.env.ADMIN_EMAIL_2,
      process.env.ADMIN_EMAIL_3,
    ].filter(Boolean) as string[],
  },
};

export default config;

Add to your .env file:

STUDIO_BASE_PATH=/api/studio ADMIN_EMAIL_1=admin@example.com ADMIN_EMAIL_2=admin2@example.com ADMIN_EMAIL_3=admin3@example.com
access.roles(optional)

Array of user roles allowed to access (e.g., ["admin", "superadmin"])

ipAddress(optional)

IP geolocation for Events and Sessions. Set provider to "ipinfo", "ipapi", or "static" (with path to your .mmdb). For ipinfo/ipapi: optional apiToken, baseUrl; ipinfo also supports endpoint: "lite" | "lookup". See the section below for details.

metadata(optional)

Custom branding and configuration options for the studio interface

metadata.title(optional)

Custom title displayed in the browser tab and application header. Default: "Better Auth Studio"

metadata.logo(optional)

URL or path to your custom logo image. Supports external URLs (http/https) or local paths. Will be displayed in the header navbar. Default: "/logo.png"

metadata.favicon(optional)

URL or path to your custom favicon. Supports multiple formats: .png, .ico, .svg, .jpg, .webp. Will be displayed in browser tabs. Default: "/logo.png"

metadata.company.name(optional)

Your company or organization name displayed in the header navbar. Default: "Better-Auth Studio."

metadata.company.website(optional)

Your company website URL. When provided, the company name in the header becomes a clickable link. Opens in a new tab with proper security attributes.

metadata.theme(optional)

Theme preference for the studio interface. Options: "light" or "dark". Default: "dark"

metadata.customStyles(optional)

Custom CSS styles to inject into the studio interface. Allows for advanced theming and customization beyond the default theme options.

💡 Example: Complete metadata configuration:

// studio.config.ts
import type { StudioConfig } from "better-auth-studio";
import { auth } from "./lib/auth";

const config: StudioConfig = {
  auth,
  basePath: process.env.STUDIO_BASE_PATH || "/api/studio",
  ipAddress: {
    provider: "ipinfo",
    apiToken: process.env.IPINFO_TOKEN,
    baseUrl: "https://api.ipinfo.io",
    endpoint: "lookup",
  },
  metadata: {
    title: "Acme Admin Dashboard",
    logo: "https://www.acme.com/logo.png",
    favicon: "https://www.acme.com/favicon.png",
    company: {
      name: "Acme",
      website: "https://www.acme.com",
    },
    theme: "dark",
    customStyles: `
      :root {
        --custom-accent-color: #00ff00;
      }
    `,
  },
  access: {
    allowEmails: [
      process.env.ADMIN_EMAIL_1,
      process.env.ADMIN_EMAIL_2,
    ].filter(Boolean) as string[],
  },
};

export default config;

Events Configuration

events.enabled(optional)

Enable event ingestion to track authentication events. When enabled, events are automatically captured and stored in your database. Default: false

events.client(optional)

Database client instance (Prisma client, Drizzle instance, Postgres pool, ClickHouse client, etc.)

events.clientType(optional)

Type of database client. Options: "prisma", "drizzle", "postgres", "sqlite", "clickhouse", "https"

events.tableName(optional)

Name of the table to store events. Default: "auth_events"

events.onEventIngest(optional)

Callback function invoked when an event is ingested. Receives the complete event object with all data (type, metadata, userId, etc.) that will be sent to the database. Useful for external actions like webhooks, analytics tracking, or custom logging.

💡 Example: Using onEventIngest callback:

// studio.config.ts
import type { StudioConfig } from "better-auth-studio";
import { auth } from "./lib/auth";
import { prisma } from "./db";

const config: StudioConfig = {
  auth,
  basePath: "/api/studio",
  events: {
    enabled: true,
    client: prisma,
    clientType: "prisma",
    tableName: "auth_events",
    onEventIngest: async (event) => {
      // event contains all data that will be sent to DB:
      // - event.id, event.type, event.timestamp
      // - event.status, event.userId, event.sessionId
      // - event.organizationId, event.metadata
      // - event.ipAddress, event.userAgent, event.source
      // - event.display.message, event.display.severity
      
      // Send to webhook
      await fetch('https://your-webhook.com/events', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(event),
      });
      
      // Track analytics
      await trackEvent(event.type, event);
    },
  },
};

export default config;
events.liveMarquee(optional)

Configuration for the live event marquee displayed at the top of the studio interface

events.liveMarquee.enabled(optional)

Enable the live event marquee. Default: true

events.liveMarquee.pollInterval(optional)

Polling interval in milliseconds for fetching new events. Default: 2000 (2 seconds)

events.liveMarquee.speed(optional)

Animation speed in pixels per frame for the scrolling marquee. Default: 0.5

events.liveMarquee.pauseOnHover(optional)

Pause the marquee animation when hovered. Default: true

events.liveMarquee.limit(optional)

Maximum number of events to display in the marquee. Default: 50

events.liveMarquee.sort(optional)

Sort order for events. Options: "desc" (newest first) or "asc" (oldest first). Default: "desc"

events.liveMarquee.colors(optional)

Custom colors for event severity types. Object with optional properties: success, info, warning, error, failed

events.liveMarquee.timeWindow(optional)

Time window for fetching events in the marquee. Can be a predefined preset or a custom duration in seconds. Default: "1h"

Options:

  • since: "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "12h" | "1d" | "2d" | "3d" | "7d" | "14d" | "30d" - Predefined time window
  • custom: number - Custom duration in seconds (e.g., 2 * 60 * 60 for 2 hours)

Note: Either since or custom must be provided, but not both.

💡 Example: Complete events configuration:

// studio.config.ts
import type { StudioConfig } from "better-auth-studio";
import { auth } from "./lib/auth";
import { prisma } from "./db";

const config: StudioConfig = {
  auth,
  basePath: "/api/studio",
  events: {
    enabled: true,
    client: prisma,
    clientType: "prisma",
    tableName: "auth_events",
    onEventIngest: async (event) => {
      // Custom logic when events are ingested
      console.log('Event ingested:', event.type);
    },
    liveMarquee: {
      enabled: true,
      pollInterval: 2000,
      speed: 1,
      pauseOnHover: true,
      limit: 10,
      sort: "desc",
      colors: {
        success: "#34d399",
        info: "#fcd34d",
        warning: "#facc15",
        error: "#f87171",
        failed: "#f87171",
      },
      timeWindow: {
        since: "1h", // Fetch events from the last hour
        // OR use custom duration:
        // custom: 2 * 60 * 60, // 2 hours in seconds
      },
    },
  },
};

export default config;

Last seen (optional)

When enabled, Studio tracks when each user was last active (sign-in or sign-up). The value is shown in the Users list and on the user details page. No Better Auth plugin is required—Studio injects the field and updates it automatically.

Enable in config:

// studio.config.ts
lastSeenAt: {
  enabled: true,
  // optional: column name on your user table (default "lastSeenAt")
  columnName: "lastSeenAt", // or "last_seen_at" etc.
},

Add a column to your user table with the same name as columnName (default lastSeenAt) as an optional datetime (nullable timestamp), then run your migration (e.g. prisma migrate dev, drizzle-kit push) or update the schema with your database client.

IP address / geolocation (optional)

To show IP geolocation (city, country) for Events and Sessions, you can use an external API via ipAddress in your studio.config.ts, or use a local MaxMind GeoLite2 database (no API key).

Supported providers

  • ipinfo — ipinfo.io (token as query param; endpoint: "lite" for free plan, endpoint: "lookup" for city/region)
  • ipapi — ipapi.co (optional token in path)
  • static — MaxMind GeoLite2 (.mmdb) file; set path to your .mmdb (e.g. your own GeoLite2 or ipdb). No API key; Studio figures out location from the file.

Example — ipinfo.io:

ipAddress: {
  provider: "ipinfo",
  apiToken: process.env.IPINFO_TOKEN,
  baseUrl: "https://api.ipinfo.io",
  endpoint: "lookup", // "lite" for free plan (country/continent only)
},

Example — ipapi.co:

ipAddress: {
  provider: "ipapi",
  baseUrl: "https://ipapi.co",
  // apiToken optional, passed in path
},

Example — MaxMind GeoLite2 .mmdb (static): If you have a DB (e.g. GeoLite2-City or your own ipdb), point to it in config. Studio will use this path and resolve locations from the file.

ipAddress: {
  provider: "static",
  path: "./data/GeoLite2-City.mmdb", // or absolute path to your .mmdb
},

Run pnpm geo:update in the studio package to download the default GeoLite2-City.mmdb into ./data/. If you omit ipAddress entirely, Studio falls back to ./data/GeoLite2-City.mmdb when present, then default-geo/ranges. Production: static works in prod as long as the .mmdb file is deployed with your app and path points to its location at runtime (relative to process cwd or absolute).

Security Notes

  • Always configure allowedEmails or allowedRoles in production
  • The studio uses encrypted session cookies for authentication
  • Admin users must sign in with email/password through the studio login page
  • Consider using environment-specific configurations for different deployment stages