API Reference ​
Complete API documentation for the Better Auth Feature Flags plugin.
API Architecture ​
The feature flags plugin follows Better Auth's architectural pattern:
- Server API: flat methods on
auth.api.*
(e.g.,auth.api.evaluateFeatureFlag
,auth.api.createFeatureFlag
) - Client API: namespaced methods on
authClient.featureFlags.*
(e.g.,authClient.featureFlags.evaluate
,authClient.featureFlags.admin.flags.create
)
Server methods are exported using keys from the plugin’s endpoints object. Client methods are derived from endpoint paths and organized under the feature-flags namespace for clarity.
Server API ​
Plugin Initialization ​
import { betterAuth } from "better-auth";
import { featureFlags } from "better-auth-feature-flags";
const auth = betterAuth({
plugins: [featureFlags(options)],
});
Evaluation API ​
evaluateFeatureFlag()
​
Evaluate a single feature flag for the current context.
const result = await auth.api.evaluateFeatureFlag({
body: {
flagKey: string,
context?: { userId?: string; organizationId?: string; attributes?: Record<string, any> },
default?: any,
select?: 'value' | 'full' | Array<'value'|'variant'|'reason'|'metadata'>,
environment?: string,
track?: boolean,
debug?: boolean,
contextInResponse?: boolean,
},
});
// Returns (default): { value, variant?, reason, metadata?, evaluatedAt, context? }
// Returns (select: 'value'): { value, evaluatedAt, context? }
Evaluation Reasons:
"rule_match"
- A targeting rule matched"override"
- User has an override"percentage_rollout"
- Percentage rollout matched"default"
- Default value returned"disabled"
- Flag is disabled"not_found"
- Flag doesn't exist
evaluateFeatureFlags()
​
Evaluate multiple flags in a single request.
const { flags } = await auth.api.evaluateFeatureFlags({
body: {
flagKeys: string[],
defaults?: Record<string, any>,
context?: { userId?: string; organizationId?: string; attributes?: Record<string, any> },
select?: 'value' | 'full' | Array<'value'|'variant'|'reason'|'metadata'>,
environment?: string,
track?: boolean,
debug?: boolean,
contextInResponse?: boolean,
},
});
// Returns: { flags: Record<string, { value: any; variant?: string; reason: string; metadata?: any }>, evaluatedAt: string, context?: object }
bootstrapFeatureFlags()
​
Get all enabled flags for a user.
const { flags, evaluatedAt, context } = await auth.api.bootstrapFeatureFlags({
body: {
context?: { userId?: string; organizationId?: string; attributes?: Record<string, any> },
include?: string[],
prefix?: string,
select?: 'value' | 'full' | Array<'value'|'variant'|'reason'|'metadata'>,
environment?: string,
track?: boolean,
debug?: boolean,
},
});
// Returns: { flags: Record<string, { value: any; variant?: string; reason: string }>|Record<string, any>, evaluatedAt: string, context: object }
Note: High-level helpers like isEnabled()
and getVariant()
are available on the client SDK.
Admin API ​
Flag Management ​
createFeatureFlag()
​
Create a new feature flag.
const flag = await auth.api.createFeatureFlag({
body: {
key: string,
name: string,
description?: string,
type: "boolean" | "string" | "number" | "json",
enabled?: boolean,
defaultValue?: any,
rolloutPercentage?: number,
organizationId?: string,
variants?: Array<{ key: string; weight: number; value: any }>,
metadata?: Record<string, any>,
},
});
// Returns: FeatureFlag
listFeatureFlags()
​
List all flags with pagination.
const { flags, page } = await auth.api.listFeatureFlags({
query: {
organizationId?: string,
cursor?: string,
limit?: number,
q?: string,
sort?: string,
type?: 'boolean'|'string'|'number'|'json',
enabled?: boolean,
prefix?: string,
include?: 'stats',
},
});
// Returns: { flags: FeatureFlag[], page: { nextCursor?: string, limit: number, hasMore: boolean } }
updateFeatureFlag()
​
Update an existing flag.
const updated = await auth.api.updateFeatureFlag({
body: { id: string, key?: string, name?: string, description?: string, enabled?: boolean, type?: FlagType, defaultValue?: any, rolloutPercentage?: number },
});
// Returns: FeatureFlag
deleteFeatureFlag()
​
Delete a flag and all associated data.
await auth.api.deleteFeatureFlag({ body: { id } });
// Returns: void
Rule Management ​
createFeatureFlagRule()
​
Add a targeting rule to a flag.
const rule = await auth.api.createFeatureFlagRule({
body: { flagId: string, priority: number, conditions: RuleConditions, value: any, variant?: string },
});
// Returns: FlagRule
Rule Conditions Structure:
interface RuleConditions {
operator?: "AND" | "OR"; // Default: "AND"
conditions: Array<
SimpleCondition | RuleConditions // Nested conditions
>;
not?: RuleConditions; // Negation
}
interface SimpleCondition {
attribute: string; // Attribute path (e.g., "user.role")
operator: ConditionOperator;
value: any;
}
type ConditionOperator =
| "equals"
| "not_equals"
| "contains"
| "not_contains"
| "starts_with"
| "ends_with"
| "in"
| "not_in"
| "greater_than"
| "less_than"
| "greater_than_or_equal"
| "less_than_or_equal"
| "regex";
listFeatureFlagRules()
​
Get all rules for a flag.
const { rules } = await auth.api.listFeatureFlagRules({
params: { flagId: string },
});
// Returns: { rules: FlagRule[] }
Override Management ​
Server-Side Overrides
These are server-side overrides for specific users, different from client-side local overrides. Server overrides persist across sessions while client overrides are temporary and blocked in production.
createFeatureFlagOverride()
​
Create a user-specific override.
const override = await auth.api.createFeatureFlagOverride({
body: { flagId: string, userId: string, value: any, enabled?: boolean, variant?: string, expiresAt?: string },
});
// Returns: FlagOverride
listFeatureFlagOverrides()
​
Get overrides for a flag or user.
const { overrides, page } = await auth.api.listFeatureFlagOverrides({
query: { flagId?: string, userId?: string, cursor?: string, limit?: number, q?: string, sort?: string },
});
<!-- Override delete endpoint is not currently provided. -->
### Analytics API
#### `createFeatureFlagEvent()`
Track custom events for analytics. (Canonical method, replaces deprecated `trackEvent()`)
```typescript
// Single event tracking with idempotency support
await auth.api.createFeatureFlagEvent({
body: {
flagKey: string,
event: string,
properties?: number | Record<string, any>,
timestamp?: string, // RFC3339
sampleRate?: number,
},
}, {
headers: { 'Idempotency-Key': 'unique-key' },
});
// Returns: { success: boolean, eventId: string }
```
#### `createFeatureFlagEventBatch()`
Track multiple events at once.
```typescript
// Batch event tracking
await auth.api.createFeatureFlagEventBatch({
body: {
events: Array<{
flagKey: string,
event: string,
properties?: number | Record<string, any>,
timestamp?: string, // RFC3339
sampleRate?: number,
}>,
sampleRate?: number,
idempotencyKey?: string,
},
});
// Returns: { success: number, failed: number, batchId: string }
```
**Idempotency Support:**
- Use `Idempotency-Key` header for single-event requests
- Use top-level `idempotencyKey` for batch requests
- Recommended for payment events, conversions, and critical analytics
<!-- Use adminGetStats endpoint documented in the server API section. -->
### Audit API
#### `listFeatureFlagAuditEntries()`
Retrieve audit log entries.
```typescript
const { entries } = await auth.api.listFeatureFlagAuditEntries({
query: {
flagId?: string,
userId?: string,
action?: "create" | "update" | "delete" | "evaluate",
start?: string,
end?: string,
limit?: number,
offset?: number,
},
});
// Returns: { entries: AuditEntry[] }
```
**Audit Actions:**
- `"flag.created"`
- `"flag.updated"`
- `"flag.deleted"`
- `"rule.created"`
- `"rule.updated"`
- `"rule.deleted"`
- `"override.created"`
- `"override.deleted"`
### Cache Management
Cache invalidation occurs automatically on admin create/update/delete operations.
## Client API
### Client Initialization
```typescript
import { createAuthClient } from "better-auth/client";
import { featureFlagsClient } from "better-auth-feature-flags/client";
const client = createAuthClient({
plugins: [featureFlagsClient(options)],
});
```
### Client Methods
#### `isEnabled()`
Check if a feature is enabled for the current user.
```typescript
const enabled: boolean = await client.featureFlags.isEnabled(
key: string,
defaultValue?: boolean
);
```
#### `getValue()`
Get the value of any flag type.
```typescript
const value: T = await client.featureFlags.getValue<T>(
key: string,
defaultValue?: T
);
```
#### `getVariant()`
Get A/B test variant assignment.
```typescript
const variant = await client.featureFlags.getVariant(
key: string
);
// Returns: string | null (variant key)
```
#### `bootstrap()`
Get all evaluated flags for the current user. (Canonical method, replaces deprecated `getAllFlags()`)
```typescript
const flags: Record<string, any> = await client.featureFlags.bootstrap();
```
#### `track()`
Track conversion or custom events with idempotency support. (Canonical method, replaces deprecated `trackEvent()`)
```typescript
// Basic event tracking
await client.featureFlags.track(
flagKey: string,
event: string,
value?: number | Record<string, any>
);
// With idempotency key (NEW in v0.2.0)
await client.featureFlags.track(
flagKey: string,
event: string,
value?: number | Record<string, any>,
{ idempotencyKey: string }
);
// Batch tracking (NEW in v0.2.0)
await client.featureFlags.trackBatch([
{
flag: string,
event: string,
data?: number | Record<string, any>,
timestamp?: Date,
idempotencyKey?: string,
}
], batchId?: string);
```
### Admin Client API (NEW in v0.2.0)
The client SDK now includes organized admin operations under the `authClient.featureFlags.admin` namespace:
```typescript
// Admin flag management
await authClient.featureFlags.admin.flags.create({ key: "new-flag", type: "boolean" });
await authClient.featureFlags.admin.flags.list();
await authClient.featureFlags.admin.flags.update("flag-id", { enabled: false });
await authClient.featureFlags.admin.flags.delete("flag-id");
// Admin rule management
await authClient.featureFlags.admin.rules.create({ flagId: "flag-id", conditions: {...} });
await authClient.featureFlags.admin.rules.list("flag-id");
// Admin override management
await authClient.featureFlags.admin.overrides.create({ flagId: "flag-id", userId: "user-123" });
await authClient.featureFlags.admin.overrides.list({ flagId: "flag-id" });
// Admin analytics
await authClient.featureFlags.admin.analytics.stats.get("flag-id");
await authClient.featureFlags.admin.analytics.usage.get();
// Admin audit
await authClient.featureFlags.admin.audit.list({ flagId: "flag-id" });
```
#### `setOverride()`
Set a local override for testing (development only).
::: danger Production Safety
Overrides are automatically disabled in production environments to prevent debug features from being exposed.
:::
```typescript
// Development only - no effect in production
client.featureFlags.setOverride(
flag: string,
value: any
);
```
#### `clearOverrides()`
Clear all local overrides.
```typescript
client.featureFlags.clearOverrides();
```
## React Hooks
### Setup
```tsx
import { FeatureFlagsProvider } from "better-auth-feature-flags/react";
function App() {
return (
<FeatureFlagsProvider client={authClient}>
{/* Your app */}
</FeatureFlagsProvider>
);
}
```
### Hooks
#### `useFeatureFlag()`
```tsx
import { useFeatureFlag } from "better-auth-feature-flags/react";
function Component() {
const isEnabled = useFeatureFlag("feature-key", false);
return isEnabled ? <NewFeature /> : <OldFeature />;
}
```
#### `useFeatureFlags()`
```tsx
import { useFeatureFlags } from "better-auth-feature-flags/react";
function Component() {
const flags = useFeatureFlags();
return (
<div>
{flags["feature-1"] && <Feature1 />}
{flags["feature-2"] === "variant-a" && <VariantA />}
</div>
);
}
```
#### `useVariant()`
```tsx
import { useVariant } from "better-auth-feature-flags/react";
function Component() {
const variant = useVariant("test-key");
switch (variant?.key) {
case "control":
return <ControlVersion />;
case "variant":
return <VariantVersion />;
default:
return <DefaultVersion />;
}
}
```
## REST API Endpoints
All endpoints are prefixed with your Better Auth API path (default: `/api/auth`).
### Public Endpoints
#### `POST /feature-flags/evaluate`
Evaluate a single flag.
Body fields (canonical):
```
{
flagKey: string,
context?: object,
default?: any,
select?: 'value'|'full'|Array<'value'|'variant'|'reason'|'metadata'>,
environment?: string,
track?: boolean,
debug?: boolean,
contextInResponse?: boolean
}
```
Headers:
- `x-deployment-ring: <env>` Optional; takes precedence over body `environment`.
Response (default):
```
{
value: any,
variant?: string,
reason: string,
metadata?: object,
evaluatedAt: string,
context?: object // when contextInResponse=true
}
```
#### `POST /feature-flags/evaluate-batch`
Evaluate multiple flags.
Body fields (canonical):
```
{
flagKeys: string[],
defaults?: Record<string, any>,
context?: object,
select?: 'value'|'full'|Array<'value'|'variant'|'reason'|'metadata'>,
environment?: string,
track?: boolean,
debug?: boolean,
contextInResponse?: boolean
}
```
Headers:
- `x-deployment-ring: <env>` Optional; takes precedence over body `environment`.
Response:
```
{
flags: Record<string, { value: any; variant?: string; reason: string; metadata?: any }> | Record<string, any>,
evaluatedAt: string,
context?: object // when contextInResponse=true
}
```
Note: `contextInResponse` defaults to `true` for batch evaluation and to `false` for single evaluation.
#### `POST /feature-flags/bootstrap`
Get all enabled flags for bootstrap/initialization. Supports server-side filtering.
Body fields (canonical):
```
{
context?: object,
include?: string[], // only include specific keys
prefix?: string, // only include keys starting with prefix
select?: 'value'|'full'|Array<'value'|'variant'|'reason'|'metadata'>,
environment?: string,
track?: boolean,
debug?: boolean
}
```
Headers:
- `x-deployment-ring: <env>` Optional; takes precedence over body `environment`.
Response:
```
{
flags: Record<string, { value: any; variant?: string; reason: string; metadata?: any }> | Record<string, any>,
evaluatedAt: string,
context: object
}
```
#### `POST /feature-flags/events`
Track a single event.
**Request Body:**
```json
{
"flagKey": "checkout-test",
"event": "purchase",
"value": 99.99
}
```
### Admin Endpoints
All admin endpoints require authentication and appropriate permissions.
Admin endpoints are under `/feature-flags/admin/...`.
#### `GET /feature-flags/admin/flags`
List flags with unified query parameters and cursor pagination.
Query parameters:
```
{
organizationId?: string,
cursor?: string,
limit?: number,
q?: string,
sort?: string, // e.g., "-updatedAt", "key"
type?: 'boolean'|'string'|'number'|'json',
enabled?: boolean,
prefix?: string,
include?: 'stats'
}
```
Response:
```
{
flags: FeatureFlag[],
page: { nextCursor?: string, limit: number, hasMore: boolean }
}
```
#### `POST /feature-flags/admin/flags`
Create a new flag.
**Request Body:**
```json
{
"key": "new-feature",
"type": "boolean",
"enabled": true,
"defaultValue": false,
"rolloutPercentage": 25
}
```
#### `GET /feature-flags/admin/flags/:id`
Get flag details.
#### `PATCH /feature-flags/admin/flags/:id`
Update a flag.
#### `DELETE /feature-flags/admin/flags/:id`
Delete a flag.
#### `GET /feature-flags/admin/flags/:flagId/rules`
Get flag rules.
#### `POST /feature-flags/admin/flags/:flagId/rules`
Create a rule.
#### `PATCH /feature-flags/admin/flags/:flagId/rules/:ruleId`
Update a rule.
#### `DELETE /feature-flags/admin/flags/:flagId/rules/:ruleId`
Delete a rule.
#### `GET /feature-flags/admin/overrides`
Get flag overrides.
#### `POST /feature-flags/admin/overrides`
Create an override.
#### `DELETE /feature-flags/admin/overrides/:id`
Delete an override.
#### `GET /feature-flags/admin/flags/:id/stats`
Get flag statistics.
#### `GET /feature-flags/admin/audit`
Get audit log.
## WebSocket API (Planned)
Real-time flag updates via WebSocket.
```typescript
// Connect to WebSocket
const ws = client.featureFlags.subscribe();
// Listen for updates
ws.on("flag:updated", (flag) => {
console.log("Flag updated:", flag.key);
});
ws.on("flag:deleted", (flagKey) => {
console.log("Flag deleted:", flagKey);
});
// Unsubscribe
ws.close();
```
## Error Handling
### Error Types
```typescript
class FeatureFlagError extends Error {
code: ErrorCode;
details?: any;
}
enum ErrorCode {
FLAG_NOT_FOUND = "FLAG_NOT_FOUND",
INVALID_FLAG_TYPE = "INVALID_FLAG_TYPE",
EVALUATION_ERROR = "EVALUATION_ERROR",
PERMISSION_DENIED = "PERMISSION_DENIED",
VALIDATION_ERROR = "VALIDATION_ERROR",
STORAGE_ERROR = "STORAGE_ERROR",
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED",
}
```
### Error Handling Example
```typescript
try {
const result = await auth.api.evaluateFeatureFlag({
body: { flagKey: "my-flag" },
});
} catch (error) {
if (error instanceof FeatureFlagError) {
switch (error.code) {
case ErrorCode.FLAG_NOT_FOUND:
// Use default value
break;
case ErrorCode.EVALUATION_ERROR:
// Log and use fallback
break;
default:
// Handle other errors
}
}
}
```
## Type Definitions
### Core Types
```typescript
interface FeatureFlag {
id: string;
key: string;
name?: string;
description?: string;
type: FlagType;
enabled: boolean;
defaultValue?: any;
rolloutPercentage?: number;
organizationId?: string;
variants?: Variant[];
metadata?: Record<string, any>;
createdAt: Date;
updatedAt: Date;
}
type FlagType = "boolean" | "string" | "number" | "json";
interface Variant {
key: string;
weight: number;
value: any;
metadata?: Record<string, any>;
}
interface FlagRule {
id: string;
flagId: string;
priority: number;
name?: string;
conditions: RuleConditions;
value: any;
percentage?: number;
enabled: boolean;
createdAt: Date;
}
interface FlagOverride {
id: string;
flagId: string;
userId: string;
value: any;
reason?: string;
expiresAt?: Date;
createdAt: Date;
}
interface EvaluationResult {
value: any;
variant?: string;
reason: EvaluationReason;
metadata?: Record<string, any>;
}
type EvaluationReason =
| "rule_match"
| "override"
| "percentage_rollout"
| "default"
| "disabled"
| "not_found";
interface EvaluationContext {
userId?: string;
email?: string;
role?: string;
organizationId?: string;
attributes?: Record<string, any>;
device?: DeviceInfo;
geo?: GeoInfo;
}
```
### Field Semantics (Authoritative)
- key
- Unique, URL‑safe identifier (alphanumeric, `-`, `_`); recommended immutable.
- Used in consistent hashing with `userId` for sticky rollout/variants.
- name
- Human‑readable display name; safe to change without affecting evaluation.
- description
- Optional long text for docs/discovery; not used in evaluation.
- type
- One of `boolean | string | number | json`. `defaultValue` and rule values should match this type.
- enabled
- When `false`, evaluation returns `defaultValue` with reason `"disabled"`.
- defaultValue
- Safe fallback when no rule/override/rollout applies. Must be compatible with `type`.
- rolloutPercentage
- Integer 0–100. Sticky assignment via consistent hashing of `userId:key`.
- If no `userId`, assignment is consistent for anonymous cohort (same hash seed).
- variants
- Array of `{ key, value, weight }`. Weights must sum to 100.
- EvaluationResult.variant is the variant key (string).
- metadata
- Free‑form JSON for tooling and UI. Not part of evaluation logic.
- organizationId
- Multi‑tenant scope. Required when multi‑tenancy is enabled.
- createdAt / updatedAt
- Timestamps. Dates in SDK types; serialized as ISO strings over HTTP.
See also: Flag authoring Do/Don’t in Quickstart for practical guidance.
## Rate Limits
Default rate limits (built-in):
| Endpoint Path | Limit | Window |
| ------------------------------------ | -------- | ------ |
| `/feature-flags/evaluate*` | 100/min | 60s |
| `/feature-flags/evaluate-batch` | 1000/min | 60s |
| `/feature-flags/admin/*` | 20/min | 60s |
Note: These defaults are defined by the plugin’s `rateLimit` settings.
## Response Codes
| Code | Description |
| ---- | --------------------- |
| 200 | Success |
| 201 | Created |
| 204 | No Content |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 409 | Conflict |
| 429 | Too Many Requests |
| 500 | Internal Server Error |
## SDK Support
Official SDKs:
- ✅ JavaScript/TypeScript
- ✅ React
- ✅ Next.js
- 🔜 Vue (planned)
- 🔜 Svelte (planned)
- 🔜 React Native (planned)
- 🔜 Flutter (planned)
## Migration Guide
### From v0.1.x to v0.2.0
```typescript
// Old API (v0.1.x) - basic setup
const enabled = await featureFlags.isEnabled("flag");
// New API (v0.2.0) - canonical naming
const result = await auth.api.evaluateFeatureFlag({
body: { flagKey: "flag" },
});
const enabled = result.value;
```
## Performance Benchmarks
| Operation | P50 | P95 | P99 |
| ----------------- | ----- | ----- | ----- |
| Single evaluation | 2ms | 10ms | 50ms |
| Batch (10 flags) | 5ms | 20ms | 100ms |
| Cache hit | 0.1ms | 0.5ms | 1ms |
| Rule evaluation | 1ms | 5ms | 20ms |
## Support
- GitHub Issues: [Report bugs](https://github.com/better-auth/plugins/issues)
- Documentation: [Full docs](https://better-auth.com/plugins/feature-flags)
- Discord: [Community support](https://discord.gg/better-auth)
Admin client plugin
- Public runtime uses `better-auth-feature-flags/client` only.
- Admin surfaces should add `better-auth-feature-flags/admin` alongside the public client.
```ts
import { createAuthClient } from "better-auth/client";
import { featureFlagsClient } from "better-auth-feature-flags/client";
import { featureFlagsAdminClient } from "better-auth-feature-flags/admin";
// Public runtime
createAuthClient({ plugins: [featureFlagsClient()] });
// Admin runtime
createAuthClient({ plugins: [featureFlagsClient(), featureFlagsAdminClient()] });
```