Ship features safely with feature flags, A/B testing, and progressive rollouts - Better Auth plugin for modern release management
npm install better-auth-feature-flags






Enterprise-grade feature flag management integrated with Better Auth. Control feature rollouts, run A/B tests, and manage user experiences with powerful targeting rules and real-time evaluation.
- ๐ Dynamic Feature Control - Enable/disable features without deployments
- ๐ฏ Advanced Targeting - Rule-based targeting with complex conditions
- ๐งช A/B Testing - Multiple variants with deterministic assignment
- ๐ Analytics & Tracking - Built-in usage analytics and performance metrics
- ๐ Progressive Rollouts - Percentage-based gradual feature releases
- ๐ Security First - Role-based access, audit logging, and context sanitization
- โก High Performance - <10ms P50 latency with intelligent caching
- ๐พ Multiple Storage Backends - Database, Memory, or Redis storage
- ๐ Smart Polling - Exponential backoff and jitter for efficient updates
- ๐ฆ Batch Evaluation - Evaluate multiple flags in a single request
- ๐ฏ Session-Aware Caching - Automatic cache invalidation on user changes
- ๐ Full TypeScript Support - Type-safe flag evaluation with schema validation
- โ๏ธ React Integration - Hooks, components, and providers
- ๐ง Development Tools - Local overrides, debug mode, and DevTools integration
- ๐ข Multi-Tenancy - Organization-level flag isolation
- ๐ Comprehensive Audit Trail - Track all changes with configurable retention
``bashnpm
npm install better-auth better-auth-feature-flags
$3
- This plugin requires
better-auth as a peer dependency.
- Install a compatible version (e.g., better-auth@^1.3.13) to ensure proper type and runtime alignment.Quick Start
$3
`typescript
import { betterAuth } from "better-auth";
import { featureFlags } from "better-auth-feature-flags";const auth = betterAuth({
plugins: [
featureFlags({
storage: "database", // "memory" | "database" | "redis"
// Define flags with automatic type inference
flags: {
"ui.dark-mode": { default: false },
"experiment.new-checkout": { default: "control" },
"config.max-items": { default: 10 },
},
// Analytics configuration
analytics: {
trackUsage: true,
trackPerformance: true,
},
// Caching for performance
caching: {
enabled: true,
ttl: 60, // seconds
maxSize: 1000,
},
// Admin access control
adminAccess: {
enabled: true,
roles: ["admin"],
},
// Multi-tenancy (optional)
multiTenant: {
enabled: false,
useOrganizations: false,
},
// Audit logging
audit: {
enabled: true,
retentionDays: 90,
},
}),
],
});
`$3
`typescript
import { createAuthClient } from "better-auth/client";
import { featureFlagsClient } from "better-auth-feature-flags/client";// Public client (for end users) - automatically inherits server schema types
const authClient = createAuthClient({
plugins: [
featureFlagsClient({
// Client-side caching
cache: {
enabled: true,
ttl: 60000, // 60 seconds
storage: "localStorage", // or "sessionStorage" or "memory"
},
// Real-time updates
polling: {
enabled: true,
interval: 30000, // 30 seconds
},
// Context collection
contextCollection: {
collectDevice: false,
collectGeo: false,
collectCustomHeaders: false,
},
}),
],
});
// TypeScript now provides full type safety based on server flag definitions
const isDarkMode = await authClient.featureFlags.isEnabled("ui.dark-mode");
// ^? boolean (inferred from server default: false)
const checkoutVariant = await authClient.featureFlags.getValue(
"experiment.new-checkout",
);
// ^? string (inferred from server default: "control")
const maxItems = await authClient.featureFlags.getValue("config.max-items");
// ^? number (inferred from server default: 10)
`#### Admin Client Setup
For admin dashboards, use both public and admin plugins:
`typescript
import { createAuthClient } from "better-auth/client";
import { featureFlagsClient } from "better-auth-feature-flags/client";
import { featureFlagsAdminClient } from "better-auth-feature-flags/admin";// Admin client (for management interfaces)
const adminClient = createAuthClient({
plugins: [
featureFlagsClient(), // Public evaluation methods
featureFlagsAdminClient(), // Admin CRUD operations
],
});
`$3
API Overview
$3
- POST
/feature-flags/evaluate
- Body: { flagKey: string, context?: object, default?: any, select?: 'value'|'full'|Array<'value'|'variant'|'reason'|'metadata'>, environment?: string, track?: boolean, debug?: boolean, contextInResponse?: boolean }
- Response (default): { value: any, variant?: string, reason: string, metadata?: any, evaluatedAt: string, context?: object }
- Response (select: 'value'): { value: any, evaluatedAt: string, context?: object }- POST
/feature-flags/evaluate-batch
- Body: { flagKeys: string[], defaults?: Record
- Response: { flags: Record- POST
/feature-flags/bootstrap
- Body: { context?: object, include?: string[], prefix?: string, select?: 'value'|'full'|Array<'value'|'variant'|'reason'|'metadata'>, environment?: string, track?: boolean, debug?: boolean }
- Server response: { flags: Record
- Client helper featureFlags.bootstrap() returns a plain keyโvalue map for convenience- POST
/feature-flags/events
- Body: { flagKey: string, event: string, properties?: number|object, timestamp?: string (RFC3339), sampleRate?: number }
- Headers: Idempotency-Key?: string
- Event tracking for analyticsNote
- Environment can also be supplied via
x-deployment-ring header; header takes precedence over body environment.
- Client bootstrap() extracts values; use server API with select if you need full result shapes.$3
- GET
/feature-flags/admin/flags
- Query: { organizationId?, cursor?, limit?, q?, sort?, type?, enabled?, prefix?, include? }
- Enhanced with filtering and metrics projection
- Response: { flags: FeatureFlag[], page: { nextCursor?, limit, hasMore } }- GET
/feature-flags/admin/flags/:flagId/stats
- Query: { start?, end?, granularity?, timezone?, metrics? }
- Analytics with date range validation (max 90 days) and selective metrics- GET
/feature-flags/admin/metrics/usage
- Query: { start?, end?, organizationId?, metrics? }
- Organization-level analytics with projection support#### Simple Flag Evaluation
`typescript
// High-level client methods (v0.2.x)
const isEnabled = await authClient.featureFlags.isEnabled("new-feature");
const value = await authClient.featureFlags.getValue(
"config-setting",
"default",
);
const variant = await authClient.featureFlags.getVariant("ab-test");// Canonical evaluation API
const result = await authClient.featureFlags.evaluate("new-feature", {
default: false,
context: { userId: "123", plan: "premium" },
});
// Returns: { value: boolean, variant?: string, reason: string }
// Batch evaluation for performance
const results = await authClient.featureFlags.evaluateMany(["flag1", "flag2"], {
context: { userId: "123" },
defaults: { flag1: false, flag2: "default" },
});
// Bootstrap all flags
const allFlags = await authClient.featureFlags.bootstrap({
context: { userId: "123" },
});
// Event tracking
await authClient.featureFlags.track("new-feature", "viewed", {
section: "dashboard",
});
`#### React Integration
`tsx
import {
FeatureFlagsProvider,
useFeatureFlag,
useFeatureFlagValue,
useVariant,
useTrackEvent,
Feature,
Variant,
} from "better-auth-feature-flags/react";// Provider setup
function App() {
return (
client={authClient}
fetchOnMount={true}
context={{ userId: "user-123", plan: "premium" }}
>
);
}
// Using hooks
function Component() {
const isDarkMode = useFeatureFlag("dark-mode", false);
const config = useFeatureFlagValue("ui-config", { theme: "light" });
const variant = useVariant("homepage-test");
const track = useTrackEvent();
const handleClick = () => {
track("ui-interaction", "button_click", { component: "header" });
};
return (
Theme: {config.theme}
Variant: {variant || "default"}
);
}// Conditional rendering
function Page() {
return (
}>
);
}
// A/B testing with variants
function Homepage() {
return (
);
}
// Suspense support for modern React apps
import {
FeatureSuspense,
useFeatureFlagSuspense,
} from "better-auth-feature-flags/react";
function SuspenseExample() {
return (
}>
}>
);
}
function SuspenseHook() {
// Throws promise for Suspense to catch
const isEnabled = useFeatureFlagSuspense("feature-name", false);
return
Feature is {isEnabled ? "enabled" : "disabled"};
}
`$3
`typescript
import { createAuthClient } from "better-auth/client";
import { featureFlagsClient } from "better-auth-feature-flags/client";const authClient = createAuthClient({
plugins: [
featureFlagsClient({
// Client-side caching
cache: {
enabled: true,
ttl: 60000, // 60 seconds
storage: "localStorage", // or "sessionStorage" or "memory"
},
// Automatic polling for updates
polling: {
enabled: true,
interval: 30000, // 30 seconds
},
// Context collection
contextCollection: {
collectDevice: false,
collectGeo: false,
collectCustomHeaders: false,
},
// Development overrides (disabled in production)
overrides: {
enabled: process.env.NODE_ENV === "development",
},
}),
],
});
`Client API Reference
$3
`typescript
// Simple boolean check
const isEnabled = await authClient.featureFlags.isEnabled("new-feature");// Get any value type with default
const config = await authClient.featureFlags.getValue("ui-config", {
theme: "light",
sidebar: "collapsed",
});
// Get variant for A/B testing
const variant = await authClient.featureFlags.getVariant("homepage-test");
// Full evaluation with metadata
const result = await authClient.featureFlags.evaluate("feature-name", {
default: false,
context: { plan: "premium", region: "us-west" },
select: "full", // Returns { value, variant?, reason }
});
// Batch evaluation
const results = await authClient.featureFlags.evaluateMany(
["feature-1", "feature-2"],
{
context: { userId: "123" },
defaults: { "feature-1": false, "feature-2": "default" },
},
);
// Bootstrap all enabled flags
const allFlags = await authClient.featureFlags.bootstrap({
context: { userId: "123" },
include: ["ui-", "experiments-"], // Optional filtering
});
`$3
`typescript
// Simple event tracking
await authClient.featureFlags.track("checkout-flow", "step_completed", {
step: "payment",
value: 99.99,
});// Context and override management
authClient.featureFlags.setContext({ plan: "premium", region: "us" });
const context = authClient.featureFlags.getContext();
// Development overrides (disabled in production)
authClient.featureFlags.setOverride("debug-mode", true);
authClient.featureFlags.clearOverrides();
// Cache management
authClient.featureFlags.clearCache();
await authClient.featureFlags.refresh();
`$3
Admin operations use a separate client plugin to keep public bundles lean:
`typescript
import { createAuthClient } from "better-auth/client";
import { featureFlagsClient } from "better-auth-feature-flags/client";
import { featureFlagsAdminClient } from "better-auth-feature-flags/admin";// Admin clients include both public and admin plugins
const adminClient = createAuthClient({
plugins: [featureFlagsClient(), featureFlagsAdminClient()],
});
// Flag management
const flags = await adminClient.featureFlags.admin.flags.list({
q: "search-term",
type: "boolean",
enabled: true,
sort: "-updatedAt",
limit: 20,
});
// Returns: { flags: FeatureFlag[], page: { nextCursor?, limit, hasMore } }
const newFlag = await adminClient.featureFlags.admin.flags.create({
key: "new-checkout",
name: "New Checkout Flow",
type: "boolean",
enabled: true,
defaultValue: false,
rolloutPercentage: 25,
variants: [
{ key: "control", value: false, weight: 50 },
{ key: "test", value: true, weight: 50 },
],
});
// Rule-based targeting
await adminClient.featureFlags.admin.rules.create({
flagId: newFlag.id,
priority: 1,
conditions: {
all: [
{ attribute: "plan", operator: "equals", value: "premium" },
{ attribute: "region", operator: "in", value: ["us", "ca"] },
],
},
value: true,
});
// Analytics with enhanced projection
const stats = await adminClient.featureFlags.admin.analytics.stats.get(
newFlag.id,
{
start: "2025-01-01",
end: "2025-01-31",
granularity: "day",
metrics: ["total", "uniqueUsers", "variants"], // Selective metrics
},
);
const usage = await adminClient.featureFlags.admin.analytics.usage.get({
start: "2025-01-01",
end: "2025-01-31",
metrics: ["errorRate", "avgLatency"], // Only get performance metrics
});
`$3
- Idempotency support for analytics events
- Batch event tracking for performance
- Canonical API naming and improved DX
- Enhanced TypeScript types and error handling
- React Suspense hooks and advanced React helpers
Migration Guide (v0.1.3 โ v0.2.0)
The v0.2.0 release introduces canonical API naming for better consistency and long-term stability. The old methods are deprecated but still supported. API renames:
-
getFlag() โ evaluate()
- getFlags() โ evaluateMany()
- getAllFlags() โ bootstrap()
- trackEvent() โ track()Old methods are deprecated but still supported for backward compatibility.
$3
| Purpose | Method |
| ------------------------- | ------------------------------------------ |
| Flag Evaluation |
featureFlags.isEnabled() |
| | featureFlags.getValue() |
| | featureFlags.getVariant() |
| | featureFlags.evaluate() |
| | featureFlags.evaluateMany() |
| | featureFlags.bootstrap() |
| Analytics | featureFlags.track() |
| | featureFlags.trackBatch() |
| Admin Operations | featureFlags.admin.flags.list() |
| | featureFlags.admin.flags.create() |
| | featureFlags.admin.flags.get() |
| | featureFlags.admin.flags.update() |
| | featureFlags.admin.flags.delete() |
| | featureFlags.admin.flags.enable() |
| | featureFlags.admin.flags.disable() |
| | featureFlags.admin.rules.list() |
| | featureFlags.admin.rules.create() |
| | featureFlags.admin.rules.get() |
| | featureFlags.admin.rules.update() |
| | featureFlags.admin.rules.delete() |
| | featureFlags.admin.rules.reorder() |
| | featureFlags.admin.overrides.list() |
| | featureFlags.admin.overrides.create() |
| | featureFlags.admin.overrides.get() |
| | featureFlags.admin.overrides.update() |
| | featureFlags.admin.overrides.delete() |
| | featureFlags.admin.audit.list() |
| | featureFlags.admin.audit.get() |
| | featureFlags.admin.analytics.stats.get() |
| | featureFlags.admin.analytics.usage.get() |
| Context | featureFlags.setContext() |
| | featureFlags.getContext() |
| Cache Management | featureFlags.clearCache() |
| | featureFlags.refresh() |
| Development Overrides | featureFlags.setOverride() |
| | featureFlags.clearOverrides() |Advanced Configuration
$3
Define flags in your server configuration for version control:
`typescript
import { betterAuth } from "better-auth";
import { featureFlags } from "better-auth-feature-flags";const auth = betterAuth({
plugins: [
featureFlags({
storage: "database",
// Static flag definitions
flags: {
"maintenance-mode": {
enabled: false,
default: false,
},
"new-checkout": {
enabled: true,
default: false,
rolloutPercentage: 25, // Gradual rollout
targeting: {
roles: ["beta-tester"],
attributes: { plan: "premium" },
},
},
"homepage-test": {
enabled: true,
variants: [
{ key: "control", value: "classic", weight: 50 },
{ key: "variant", value: "modern", weight: 50 },
],
},
},
// Analytics configuration
analytics: {
trackUsage: true,
trackPerformance: true,
},
// Multi-tenancy
multiTenant: {
enabled: true,
useOrganizations: true,
},
// Admin access control
adminAccess: {
enabled: true,
roles: ["admin", "feature-manager"],
},
}),
],
});
`$3
`typescript
// Database storage (recommended for production)
featureFlags({ storage: "database" });// Memory storage (development/testing)
featureFlags({ storage: "memory" });
// Redis storage (high-scale distributed)
featureFlags({ storage: "redis" });
`Best Practices
$3
โ
Do:
- Use descriptive, URL-safe keys:
new-checkout, experiment-homepage
- Start with enabled: false and safe defaults
- Use gradual rollouts: rolloutPercentage: 10 โ 25 โ 50 โ 100
- Define meaningful variant keys: control, variant-a, new-design
- Scope flags by organization in multi-tenant environmentsโ Don't:
- Include PII or secrets in flag metadata
- Change flag keys after deployment (breaks analytics)
- Use chaotic on/off toggling (prefer rollout percentages)
- Omit weights in variants (must sum to 100)
$3
`typescript
// Use caching for better performance
featureFlagsClient({
cache: {
enabled: true,
ttl: 60000, // 1 minute
storage: "localStorage",
}, // Enable polling for real-time updates
polling: {
enabled: true,
interval: 30000, // 30 seconds
},
});
// Batch evaluations when possible
const results = await client.featureFlags.evaluateMany([
"feature-1",
"feature-2",
"feature-3",
]);
// Use bootstrap for initial page load
const allFlags = await client.featureFlags.bootstrap();
`$3
- Context sanitization is enabled by default
- Production overrides are automatically disabled
- Admin operations require proper role-based access
- Audit logging tracks all flag changes
$3
The plugin provides two approaches for type safety: automatic inference and explicit schemas.
#### Option 1: Automatic Type Inference (Recommended)
`typescript
// Server setup with automatic schema inference from flag definitions
const auth = betterAuth({
plugins: [
featureFlags({
storage: "database",
flags: {
"ui.dark-mode": { default: false }, // inferred as boolean
"experiment.homepage": { default: "control" }, // inferred as string
"config.max-items": { default: 10 }, // inferred as number
"feature.premium-checkout": { default: false }, // inferred as boolean
},
}),
],
});
// ^? Types are automatically inferred from default values// Client automatically inherits server schema types
const client = createAuthClient({
plugins: [featureFlagsClient()], // No manual schema needed!
});
// TypeScript provides full type safety with zero configuration
const isDark = await client.featureFlags.isEnabled("ui.dark-mode");
// ^? boolean (inferred from server default: false)
const variant = await client.featureFlags.getValue("experiment.homepage");
// ^? string (inferred from server default: "control")
const maxItems = await client.featureFlags.getValue("config.max-items");
// ^? number (inferred from server default: 10)
`#### Option 2: Explicit Schema Definition
`typescript
// Define your flag schema for complex types
interface AppFlags {
"ui.dark-mode": boolean;
"experiment.homepage": "control" | "variant-a" | "variant-b";
"config.max-items": number;
"feature.premium-checkout": boolean;
}// Server setup with explicit typed schema
const auth = betterAuth({
plugins: [
featureFlags({
storage: "database",
// Optional: define static flags (will be type-checked against AppFlags)
flags: {
"ui.dark-mode": { default: false },
},
}),
],
});
// Type-safe client with explicit schema
const client = createAuthClient({
plugins: [featureFlagsClient()],
});
// TypeScript ensures exact type matching
const variant = await client.featureFlags.getValue(
"experiment.homepage",
"control",
);
// ^? "control" | "variant-a" | "variant-b" (from AppFlags interface)
`#### Mixed Approach: Best of Both Worlds
`typescript
// Define base schema for complex types
interface AppFlags {
"experiment.homepage": "control" | "variant-a" | "variant-b";
}// Server combines explicit schema with inferred flags
const auth = betterAuth({
plugins: [
featureFlags({
storage: "database",
flags: {
// Explicit schema type (union)
"experiment.homepage": { default: "control" },
// Auto-inferred types
"ui.dark-mode": { default: false }, // โ boolean
"config.max-items": { default: 10 }, // โ number
},
}),
],
});
`Performance & Security
$3
- Evaluation Latency: <10ms P50, <100ms P99
- Throughput: 100,000+ evaluations/second
- Cache Hit Rate: >95% with proper configuration
- Bundle Size: ~5KB minified + gzipped (core + React)
$3
- Context Sanitization: Automatic PII filtering and validation
- Production Safeguards: Development overrides disabled in production
- Role-Based Access: Admin operations require proper authentication
- Audit Trail: Complete change history with configurable retention
- Multi-Tenant Isolation: Organization-level flag scoping
Documentation
๐ Full Documentation
- Quickstart Guide - Get up and running in 5 minutes
- Configuration - Detailed configuration options
- API Reference - Complete API documentation
- Client SDK - Frontend integration guide
- Device Detection - Target by device, browser, OS
- Troubleshooting - Common issues and solutions
Architecture
$3
The feature flags plugin uses a modular architecture for better maintainability and performance:
#### Public Endpoints (by functional concern)
`
src/endpoints/public/
โโโ evaluate.ts # Single flag evaluation
โโโ evaluate-batch.ts # Batch flag evaluation
โโโ bootstrap.ts # Bulk flag initialization
โโโ events.ts # Analytics event tracking
โโโ config.ts # Public configuration
โโโ health.ts # Service health checks
`#### Admin Endpoints (by resource type)
`
src/endpoints/admin/
โโโ flags.ts # Flag CRUD operations
โโโ rules.ts # Rule management
โโโ overrides.ts # Override management
โโโ analytics.ts # Stats and metrics
โโโ audit.ts # Audit log access
โโโ environments.ts # Environment management + data export
`$3
- Single Responsibility: Each module focuses on one concern (200-300 lines)
- Better Tree-Shaking: Unused admin features don't bloat client bundles
- Easier Testing: Focused test suites per module
- Independent Development: Teams can work on different modules without conflicts
- Clear API Surface: RESTful organization makes the API predictable
$3
- Public endpoints organized by functional concern for performance optimization
- Admin endpoints organized by REST resource for consistent management UX
- Shared utilities in
endpoints/shared.ts for consistent security and validation
- Composition pattern in endpoints/index.ts` to maintain backward compatibility| Feature | Better Auth Feature Flags | LaunchDarkly | Unleash | Flagsmith |
| --------------------------- | ------------------------- | ------------ | ---------- | ---------- |
| Open Source | โ
| โ | โ
| โ
|
| Self-hosted | โ
| โ | โ
| โ
|
| Type Safety | โ
Full | โ ๏ธ Partial | โ ๏ธ Partial | โ ๏ธ Partial |
| Better Auth Integration | โ
Native | โ | โ | โ |
| Smart Caching | โ
| โ
| โ ๏ธ Basic | โ ๏ธ Basic |
| A/B Testing | โ
| โ
| โ
| โ
|
| Audit Logging | โ
| โ
| โ
| โ
|
| Multi-tenancy | โ
| โ
| โ ๏ธ Limited | โ
|
| Device Detection | โ
| โ ๏ธ Limited | โ | โ |
| Pricing | Free | $$$ | Free/$ | Free/$ |
- ๐ฎ Discord: Join our community
- ๐ฌ Discussions: GitHub Discussions
- ๐ Issues: GitHub Issues
- ๐ Sponsor: GitHub Sponsors
We welcome contributions! Please see our Contributing Guide for details.
This project is made possible by our generous sponsors. Thank you for your support! ๐
MIT - See LICENSE for details