import "server-only"; import { prisma } from "@/lib/db"; /** * Feature flags with runtime effect. * * Flags are stored in the `feature_flag` table (admin-toggleable, no deploy) and * READ here to actually gate behavior. Known flags have a registry entry that * supplies a label, description, and a safe default when the row doesn't exist * yet. Admins may also create ad-hoc custom flags from the admin UI. */ export interface FlagDef { key: string; label: string; description: string; default: boolean; } export const FLAG_REGISTRY: FlagDef[] = [ { key: "signups_enabled", label: "Public sign-ups", description: "Allow new users to create an account. Turn off to pause registration.", default: true, }, { key: "episode_generation_enabled", label: "Episode generation", description: "Master kill-switch for AI generation (new episodes, regenerations, series). Turn off during incidents or provider outages.", default: true, }, { key: "ai_moderation_enabled", label: "AI content moderation", description: "Screen episode topics and generated scripts with OpenAI moderation; flag violations for review.", default: true, }, { key: "maintenance_banner", label: "Maintenance banner", description: "Show a site-wide notice in the app that maintenance is in progress.", default: false, }, ]; const DEFAULTS = new Map(FLAG_REGISTRY.map((f) => [f.key, f.default])); // Short per-process cache so hot paths (layout, generation) don't hit the DB on // every request. Admin toggles propagate within TTL_MS; the admin UI itself // reads fresh (uncached) data via getAllFlags(). const TTL_MS = 10_000; let cache: { at: number; map: Map } | null = null; async function loadFlags(): Promise> { if (cache && Date.now() - cache.at < TTL_MS) return cache.map; const rows = await prisma.featureFlag.findMany({ select: { key: true, enabled: true } }); const map = new Map(rows.map((r) => [r.key, r.enabled])); cache = { at: Date.now(), map }; return map; } /** Resolve a flag: DB value if present, else the registry default (else false). */ export async function isFlagEnabled(key: string): Promise { const map = await loadFlags(); if (map.has(key)) return map.get(key)!; return DEFAULTS.get(key) ?? false; } /** Invalidate the in-process cache (call after an admin toggles a flag). */ export function bustFlagCache(): void { cache = null; } export interface FlagView { key: string; label: string; description: string; enabled: boolean; known: boolean; } /** Fresh (uncached) merged view of known + custom flags for the admin screen. */ export async function getAllFlags(): Promise { const rows = await prisma.featureFlag.findMany(); const byKey = new Map(rows.map((r) => [r.key, r.enabled])); const known: FlagView[] = FLAG_REGISTRY.map((f) => ({ key: f.key, label: f.label, description: f.description, enabled: byKey.get(f.key) ?? f.default, known: true, })); const custom: FlagView[] = rows .filter((r) => !DEFAULTS.has(r.key)) .map((r) => ({ key: r.key, label: r.key, description: "Custom flag (no registry entry — not read by application code).", enabled: r.enabled, known: false, })); return [...known, ...custom]; }