better-auth-feature-flags
Version:
Ship features safely with feature flags, A/B testing, and progressive rollouts - Better Auth plugin for modern release management
974 lines (968 loc) • 37.1 kB
TypeScript
import * as better_auth_client from 'better-auth/client';
import * as better_call from 'better-call';
import * as better_auth from 'better-auth';
import * as z from 'zod';
import { F as FeatureFlagsOptions, E as EvaluationContext } from './types-_f512wNn.js';
export { B as BooleanFlags, I as InferFlagValue, i as ValidateFlagSchema } from './types-_f512wNn.js';
/**
* Secure flag override manager with environment detection.
* @security Blocks production overrides unless explicitly enabled
* @usage Debug/testing environments only by default
*/
interface OverrideConfig {
/** DANGEROUS: Allow overrides in production */
allowInProduction?: boolean;
/** Override TTL in ms (default: 1 hour) */
ttl?: number;
/** Persist to localStorage */
persist?: boolean;
/** Storage key prefix */
keyPrefix?: string;
/** Override environment detection for testing */
environment?: "development" | "production";
}
/** Creates Better Auth plugin for feature flags with full configuration */
declare function createFeatureFlagsPlugin(options?: FeatureFlagsOptions): {
id: "feature-flags";
schema: {
featureFlag: {
modelName: string;
fields: {
key: {
type: "string";
required: true;
unique: true;
};
name: {
type: "string";
required: true;
};
description: {
type: "string";
required: false;
};
type: {
type: "string";
defaultValue: string;
validator: {
input: z.ZodEnum<{
string: "string";
number: "number";
boolean: "boolean";
json: "json";
}>;
output: z.ZodEnum<{
string: "string";
number: "number";
boolean: "boolean";
json: "json";
}>;
};
};
enabled: {
type: "boolean";
defaultValue: false;
};
defaultValue: {
type: "string";
required: false;
transform: {
input(value: any): string | null;
output(value: any): any;
};
};
rolloutPercentage: {
type: "number";
defaultValue: number;
validator: {
input: z.ZodNumber;
output: z.ZodNumber;
};
};
organizationId: {
type: "string";
references: {
model: string;
field: string;
onDelete: "cascade";
};
required: false;
};
createdAt: {
type: "date";
required: true;
defaultValue: () => Date;
};
updatedAt: {
type: "date";
required: true;
defaultValue: () => Date;
};
variants: {
type: "string";
required: false;
transform: {
input(value: any): string | null;
output(value: any): any;
};
};
metadata: {
type: "string";
required: false;
transform: {
input(value: any): string | null;
output(value: any): any;
};
};
};
};
flagRule: {
modelName: string;
fields: {
flagId: {
type: "string";
references: {
model: string;
field: string;
onDelete: "cascade";
};
required: true;
};
priority: {
type: "number";
defaultValue: number;
validator: {
input: z.ZodNumber;
output: z.ZodNumber;
};
};
name: {
type: "string";
required: false;
};
conditions: {
type: "string";
required: true;
transform: {
input(value: any): string;
output(value: any): any;
};
};
value: {
type: "string";
required: true;
transform: {
input(value: any): string;
output(value: any): any;
};
};
percentage: {
type: "number";
required: false;
validator: {
input: z.ZodOptional<z.ZodNumber>;
output: z.ZodOptional<z.ZodNumber>;
};
};
enabled: {
type: "boolean";
defaultValue: true;
};
createdAt: {
type: "date";
required: true;
defaultValue: () => Date;
};
};
};
flagOverride: {
modelName: string;
fields: {
flagId: {
type: "string";
references: {
model: string;
field: string;
onDelete: "cascade";
};
required: true;
};
userId: {
type: "string";
references: {
model: string;
field: string;
onDelete: "cascade";
};
required: true;
};
value: {
type: "string";
required: true;
transform: {
input(value: any): string;
output(value: any): any;
};
};
enabled: {
type: "boolean";
defaultValue: true;
};
variant: {
type: "string";
required: false;
};
reason: {
type: "string";
required: false;
};
expiresAt: {
type: "date";
required: false;
};
createdAt: {
type: "date";
required: true;
defaultValue: () => Date;
};
};
};
flagEvaluation: {
modelName: string;
fields: {
flagId: {
type: "string";
references: {
model: string;
field: string;
onDelete: "cascade";
};
required: true;
};
userId: {
type: "string";
references: {
model: string;
field: string;
onDelete: "set null";
};
required: false;
};
sessionId: {
type: "string";
required: false;
};
value: {
type: "string";
required: true;
transform: {
input(value: any): string;
output(value: any): any;
};
};
variant: {
type: "string";
required: false;
};
reason: {
type: "string";
required: false;
validator: {
input: z.ZodOptional<z.ZodEnum<{
default: "default";
rule_match: "rule_match";
override: "override";
percentage_rollout: "percentage_rollout";
disabled: "disabled";
not_found: "not_found";
}>>;
output: z.ZodOptional<z.ZodEnum<{
default: "default";
rule_match: "rule_match";
override: "override";
percentage_rollout: "percentage_rollout";
disabled: "disabled";
not_found: "not_found";
}>>;
};
};
context: {
type: "string";
required: false;
transform: {
input(value: any): string | null;
output(value: any): any;
};
};
evaluatedAt: {
type: "date";
required: true;
defaultValue: () => Date;
};
};
};
flagAudit: {
modelName: string;
fields: {
flagId: {
type: "string";
references: {
model: string;
field: string;
onDelete: "cascade";
};
required: true;
};
userId: {
type: "string";
references: {
model: string;
field: string;
onDelete: "set null";
};
required: false;
};
action: {
type: "string";
required: true;
validator: {
input: z.ZodEnum<{
enabled: "enabled";
disabled: "disabled";
created: "created";
updated: "updated";
deleted: "deleted";
rule_added: "rule_added";
rule_updated: "rule_updated";
rule_deleted: "rule_deleted";
override_added: "override_added";
override_removed: "override_removed";
}>;
output: z.ZodEnum<{
enabled: "enabled";
disabled: "disabled";
created: "created";
updated: "updated";
deleted: "deleted";
rule_added: "rule_added";
rule_updated: "rule_updated";
rule_deleted: "rule_deleted";
override_added: "override_added";
override_removed: "override_removed";
}>;
};
};
previousValue: {
type: "string";
required: false;
transform: {
input(value: any): string | null;
output(value: any): any;
};
};
newValue: {
type: "string";
required: false;
transform: {
input(value: any): string | null;
output(value: any): any;
};
};
metadata: {
type: "string";
required: false;
transform: {
input(value: any): string | null;
output(value: any): any;
};
};
createdAt: {
type: "date";
required: true;
defaultValue: () => Date;
};
};
};
};
init(auth: better_auth.AuthContext): {};
readonly hooks: {
before: {
matcher: (ctx: any) => any;
handler: (ctx: any) => Promise<any>;
}[];
after: {
matcher: (ctx: any) => any;
handler: (ctx: any) => Promise<any>;
}[];
};
endpoints: {
[key: string]: better_call.Endpoint;
};
rateLimit: {
window: number;
max: number;
pathMatcher: (path: string) => boolean;
}[];
$ERROR_CODES: {
FLAG_NOT_FOUND: string;
INVALID_FLAG_TYPE: string;
EVALUATION_ERROR: string;
STORAGE_ERROR: string;
UNAUTHORIZED_ACCESS: string;
INVALID_CONTEXT: string;
QUOTA_EXCEEDED: string;
INVALID_INPUT: string;
ADMIN_ACCESS_DISABLED: string;
ORGANIZATION_REQUIRED: string;
};
};
type FeatureFlagsServerPlugin = ReturnType<typeof createFeatureFlagsPlugin>;
interface FeatureFlagVariant {
/** Unique variant identifier */
key: string;
/** Variant value of any type */
value: any;
/** Rollout percentage weight (0-100) */
weight?: number;
}
interface FeatureFlagResult {
/** Evaluated flag value */
value: any;
/** Variant key returned by server */
variant?: string;
/** Evaluation reason for debugging */
reason: "default" | "rule_match" | "override" | "percentage_rollout" | "not_found" | "disabled";
}
/**
* Feature flags client configuration options.
*
* @template Schema - Optional flag schema for type safety
*/
interface FeatureFlagsClientOptions<Schema extends Record<string, any> = Record<string, any>> {
/** Client-side flag caching for performance and offline support */
cache?: {
enabled?: boolean;
/** TTL in ms - flags re-evaluate server-side after expiry */
ttl?: number;
storage?: "memory" | "localStorage" | "sessionStorage";
keyPrefix?: string;
/** Increment to bust cache across deployments */
version?: string;
/** Maximum cache entries (default: 100) */
maxEntries?: number;
/** Whitelist: only these flags are cached */
include?: string[];
/** Blacklist: these flags bypass cache (e.g., high-frequency A/B tests) */
exclude?: string[];
};
/** Smart polling with exponential backoff, prevents thundering herd */
polling?: {
enabled?: boolean;
/** Base interval in ms - backs off exponentially on errors */
interval?: number;
};
/** Default flag values for fallback */
defaults?: Partial<Schema>;
/** Enable debug logging */
debug?: boolean;
/** Global error handler for flag evaluation failures */
onError?: (error: Error) => void;
/** Called after each flag evaluation for analytics */
onEvaluation?: (flag: string, result: any) => void;
/** Context sanitization - prevents PII leakage and enforces size limits */
contextSanitization?: {
/** Default: true */
enabled?: boolean;
/** Only allow whitelisted fields (default: true) */
strict?: boolean;
/** Additional allowed fields beyond defaults */
allowedFields?: string[];
/** Max context size for GET requests (default: 2KB) */
maxUrlSize?: number;
/** Max context size for POST requests (default: 10KB) */
maxBodySize?: number;
/** Log warnings when fields are dropped */
warnOnDrop?: boolean;
};
/** Override config for local testing - auto-disabled in production */
overrides?: OverrideConfig;
}
/**
* Type-safe feature flags client interface.
*
* @template Schema - Optional flag schema for type safety
* @example
* interface MyFlags {
* "feature.darkMode": boolean;
* "experiment.algorithm": "A" | "B" | "C";
* }
* const client: FeatureFlagsClient<MyFlags> = createAuthClient();
*/
interface FeatureFlagsClient<Schema extends Record<string, any> = Record<string, any>> {
featureFlags: {
/** Check if boolean flag is enabled */
isEnabled: <K extends keyof Schema>(flag: K & {
[P in K]: Schema[P] extends boolean ? K : never;
}[K], defaultValue?: boolean) => Promise<boolean>;
/** Get typed flag value with fallback */
getValue: <K extends keyof Schema>(flag: K, defaultValue?: Schema[K]) => Promise<Schema[K]>;
/** Get variant key for A/B tests */
getVariant: <K extends keyof Schema>(flag: K) => Promise<string | null>;
/** Evaluate single flag */
evaluate: <K extends keyof Schema>(flag: K, options?: {
default?: Schema[K];
context?: EvaluationContext;
environment?: string;
select?: "value" | "full" | Array<"value" | "variant" | "reason" | "metadata">;
contextInResponse?: boolean;
}) => Promise<FeatureFlagResult>;
/** Evaluate multiple flags efficiently */
evaluateMany: <K extends keyof Schema>(flags: K[], options?: {
defaults?: Partial<Record<K, Schema[K]>>;
context?: EvaluationContext;
environment?: string;
select?: "value" | "full" | Array<"value" | "variant" | "reason" | "metadata">;
contextInResponse?: boolean;
}) => Promise<Record<K, FeatureFlagResult>>;
/** Bootstrap all flags for client */
bootstrap: (options?: {
context?: EvaluationContext;
environment?: string;
include?: string[];
prefix?: string;
select?: "value" | "full" | Array<"value" | "variant" | "reason" | "metadata">;
contextInResponse?: boolean;
}) => Promise<Partial<Schema>>;
/** Track flag events for analytics */
track: <K extends keyof Schema>(flag: K, event: string, value?: number | Record<string, any>, options?: {
idempotencyKey?: string;
timestamp?: Date;
sampleRate?: number;
debug?: boolean;
}) => Promise<{
success: boolean;
eventId: string;
sampled?: boolean;
}>;
/** Track multiple flag events in a single batch for efficiency */
trackBatch: <K extends keyof Schema>(events: Array<{
flag: K;
event: string;
data?: number | Record<string, any>;
timestamp?: Date;
sampleRate?: number;
}>, options?: {
idempotencyKey?: string;
sampleRate?: number;
}) => Promise<{
success: number;
failed: number;
sampled?: number;
batchId: string;
}>;
/** Set evaluation context (session auto-managed) */
setContext: (context: Partial<EvaluationContext>) => void;
/** Get current evaluation context */
getContext: () => EvaluationContext;
/** Warm cache for known flags */
prefetch: <K extends keyof Schema>(flags: K[]) => Promise<void>;
/** Clear all cached flags */
clearCache: () => void;
/** Override flag value for local testing */
setOverride: <K extends keyof Schema>(flag: K, value: Schema[K]) => void;
/** Clear all local overrides */
clearOverrides: () => void;
/** Force refresh all flags from server */
refresh: () => Promise<void>;
/** Subscribe to flag changes */
subscribe: (callback: (flags: Partial<Schema>) => void) => () => void;
/** Cleanup resources and stop polling */
dispose?: () => void;
admin: {
/** Flags management */
flags: {
list: (options?: {
organizationId?: string;
cursor?: string;
limit?: number;
q?: string;
sort?: string;
include?: "stats";
}) => Promise<{
flags: any[];
page: {
nextCursor?: string;
limit: number;
hasMore: boolean;
};
}>;
create: (flag: {
key: string;
name: string;
description?: string;
enabled?: boolean;
type: "string" | "number" | "boolean" | "json";
defaultValue: any;
rolloutPercentage?: number;
organizationId?: string;
}) => Promise<any>;
get: (id: string) => Promise<any>;
update: (id: string, updates: {
key?: string;
name?: string;
description?: string;
enabled?: boolean;
type?: "string" | "number" | "boolean" | "json";
defaultValue?: any;
rolloutPercentage?: number;
}) => Promise<any>;
delete: (id: string) => Promise<{
success: boolean;
}>;
enable: (id: string) => Promise<any>;
disable: (id: string) => Promise<any>;
};
/** Rules management */
rules: {
list: (flagId: string) => Promise<{
rules: any[];
}>;
create: (rule: {
flagId: string;
priority: number;
conditions: any;
value: any;
variant?: string;
}) => Promise<any>;
get: (flagId: string, ruleId: string) => Promise<any>;
update: (flagId: string, ruleId: string, updates: any) => Promise<any>;
delete: (flagId: string, ruleId: string) => Promise<any>;
reorder: (flagId: string, ids: string[]) => Promise<any>;
};
/** Overrides management */
overrides: {
list: (options?: {
organizationId?: string;
cursor?: string;
limit?: number;
q?: string;
sort?: string;
flagId?: string;
userId?: string;
}) => Promise<{
overrides: any[];
page: {
nextCursor?: string;
limit: number;
hasMore: boolean;
};
}>;
create: (override: {
flagId: string;
userId: string;
value: any;
enabled?: boolean;
variant?: string;
expiresAt?: string;
}) => Promise<any>;
get: (id: string) => Promise<any>;
update: (id: string, updates: any) => Promise<any>;
delete: (id: string) => Promise<any>;
};
/** Analytics */
analytics: {
stats: {
get: (flagId: string, options?: {
granularity?: "hour" | "day" | "week" | "month";
start?: string;
end?: string;
timezone?: string;
}) => Promise<{
stats: any;
}>;
};
usage: {
get: (options?: {
start?: string;
end?: string;
timezone?: string;
organizationId?: string;
}) => Promise<{
usage: any;
}>;
};
};
/** Audit logs */
audit: {
list: (options: {
flagId?: string;
userId?: string;
action?: "create" | "update" | "delete" | "evaluate";
startDate?: string;
endDate?: string;
limit?: number;
offset?: number;
}) => Promise<{
entries: any[];
}>;
get: (id: string) => Promise<any>;
};
/** Environments */
environments: {
list: () => Promise<{
environments: any[];
}>;
create: (env: any) => Promise<any>;
update: (id: string, updates: any) => Promise<any>;
delete: (id: string) => Promise<any>;
};
/** Data exports */
exports: {
create: (options: any) => Promise<any>;
};
};
};
}
/**
* Creates a type-safe feature flags client plugin for Better Auth.
*
* @template Schema - Optional flag schema for type safety
* @see src/endpoints/ for server implementation
*/
declare function featureFlagsClient<Schema extends Record<string, any> = Record<string, any>>(options?: FeatureFlagsClientOptions<Schema>): {
id: "feature-flags";
$InferServerPlugin: FeatureFlagsServerPlugin;
pathMethods: {
"/feature-flags/evaluate": "POST";
"/feature-flags/evaluate-batch": "POST";
"/feature-flags/bootstrap": "POST";
"/feature-flags/events": "POST";
"/feature-flags/events/batch": "POST";
"/feature-flags/config": "GET";
"/feature-flags/health": "GET";
"/feature-flags/admin/flags": "GET";
"/feature-flags/admin/flags/:id": "GET";
"/feature-flags/admin/flags/:id/enable": "POST";
"/feature-flags/admin/flags/:id/disable": "POST";
"/feature-flags/admin/flags/:flagId/rules": "GET";
"/feature-flags/admin/flags/:flagId/rules/:ruleId": "GET";
"/feature-flags/admin/flags/:flagId/rules/reorder": "POST";
"/feature-flags/admin/flags/:flagId/stats": "GET";
"/feature-flags/admin/overrides": "GET";
"/feature-flags/admin/overrides/:id": "GET";
"/feature-flags/admin/metrics/usage": "GET";
"/feature-flags/admin/audit": "GET";
"/feature-flags/admin/audit/:id": "GET";
"/feature-flags/admin/environments": "GET";
"/feature-flags/admin/environments/:id": "GET";
"/feature-flags/admin/export": "POST";
};
getAtoms: (..._args: any[]) => {};
getActions: (fetch: any, $store: any, _clientOptions: better_auth_client.ClientOptions | undefined) => {
featureFlags: {
isEnabled<K extends keyof Schema>(flag: K & { [P in K]: Schema[P] extends boolean ? K : never; }[K], defaultValue?: boolean): Promise<boolean>;
getValue<K extends keyof Schema>(flag: K, defaultValue?: Schema[K]): Promise<Schema[K]>;
getVariant<K extends keyof Schema>(flag: K): Promise<string | null>;
evaluate<K extends keyof Schema>(flag: K, opts?: {
default?: Schema[K];
context?: EvaluationContext;
environment?: string;
select?: "value" | "full" | Array<"value" | "variant" | "reason" | "metadata">;
contextInResponse?: boolean;
track?: boolean;
debug?: boolean;
}): Promise<FeatureFlagResult>;
evaluateMany<K extends keyof Schema>(keys: K[], opts?: {
defaults?: Partial<Record<K, Schema[K]>>;
context?: EvaluationContext;
environment?: string;
select?: "value" | "full" | Array<"value" | "variant" | "reason" | "metadata">;
contextInResponse?: boolean;
track?: boolean;
debug?: boolean;
}): Promise<Record<K, FeatureFlagResult>>;
bootstrap(options?: {
context?: EvaluationContext;
environment?: string;
include?: string[];
prefix?: string;
select?: "value" | "full" | Array<"value" | "variant" | "reason" | "metadata">;
contextInResponse?: boolean;
track?: boolean;
debug?: boolean;
defaults?: Partial<Schema>;
}): Promise<Partial<Schema>>;
track<K extends keyof Schema>(flag: K, event: string, value?: number | Record<string, any>, options?: {
idempotencyKey?: string;
timestamp?: Date;
sampleRate?: number;
debug?: boolean;
}): Promise<{
success: boolean;
eventId: string;
sampled?: boolean;
}>;
trackBatch<K extends keyof Schema>(events: Array<{
flag: K;
event: string;
data?: number | Record<string, any>;
timestamp?: Date;
sampleRate?: number;
}>, options?: {
idempotencyKey?: string;
sampleRate?: number;
}): Promise<{
success: number;
failed: number;
sampled?: number;
batchId: string;
}>;
setContext(newContext: Partial<EvaluationContext>): void;
getContext(): EvaluationContext;
prefetch<K extends keyof Schema>(flags: K[]): Promise<void>;
clearCache(): void;
setOverride<K extends keyof Schema>(flag: K, value: Schema[K]): void;
clearOverrides(): void;
refresh(): Promise<void>;
subscribe(callback: (flags: Partial<Schema>) => void): () => void;
admin: {
flags: {
list(options?: {
organizationId?: string;
cursor?: string;
limit?: number;
q?: string;
sort?: string;
include?: "stats";
}): Promise<{
flags: any[];
page: {
nextCursor?: string;
limit: number;
hasMore: boolean;
};
}>;
create(flag: {
key: string;
name: string;
description?: string;
enabled?: boolean;
type: "string" | "number" | "boolean" | "json";
defaultValue: any;
rolloutPercentage?: number;
organizationId?: string;
}): Promise<any>;
get(id: string): Promise<any>;
update(id: string, updates: {
key?: string;
name?: string;
description?: string;
enabled?: boolean;
type?: "string" | "number" | "boolean" | "json";
defaultValue?: any;
rolloutPercentage?: number;
}): Promise<any>;
delete(id: string): Promise<{
success: boolean;
}>;
enable(id: string): Promise<any>;
disable(id: string): Promise<any>;
};
rules: {
list(flagId: string): Promise<{
rules: any[];
}>;
create(rule: {
flagId: string;
priority: number;
conditions: any;
value: any;
variant?: string;
}): Promise<any>;
get(flagId: string, ruleId: string): Promise<any>;
update(flagId: string, ruleId: string, updates: any): Promise<any>;
delete(flagId: string, ruleId: string): Promise<any>;
reorder(flagId: string, ids: string[]): Promise<any>;
};
overrides: {
list(options?: {
organizationId?: string;
cursor?: string;
limit?: number;
q?: string;
sort?: string;
flagId?: string;
userId?: string;
}): Promise<{
overrides: any[];
page: {
nextCursor?: string;
limit: number;
hasMore: boolean;
};
}>;
create(override: {
flagId: string;
userId: string;
value: any;
enabled?: boolean;
variant?: string;
expiresAt?: string;
}): Promise<any>;
get(id: string): Promise<any>;
update(id: string, updates: any): Promise<any>;
delete(id: string): Promise<any>;
};
analytics: {
stats: {
get(flagId: string, options?: {
granularity?: "hour" | "day" | "week" | "month";
start?: string;
end?: string;
timezone?: string;
}): Promise<{
stats: any;
}>;
};
usage: {
get(options?: {
start?: string;
end?: string;
timezone?: string;
organizationId?: string;
}): Promise<{
usage: any;
}>;
};
};
audit: {
list(_options: {
flagId?: string;
userId?: string;
action?: "create" | "update" | "delete" | "evaluate";
startDate?: string;
endDate?: string;
limit?: number;
offset?: number;
}): Promise<{
entries: any[];
}>;
get(id: string): Promise<any>;
};
environments: {
list(): Promise<{
environments: any[];
}>;
create(env: any): Promise<any>;
update(id: string, updates: any): Promise<any>;
delete(id: string): Promise<any>;
};
exports: {
create(options: any): Promise<any>;
};
};
dispose(): void;
};
};
};
export { EvaluationContext, type FeatureFlagResult, type FeatureFlagVariant, type FeatureFlagsClient, type FeatureFlagsClientOptions, featureFlagsClient as default, featureFlagsClient };