feat(flags): seed default feature flags for all products on startup

This commit is contained in:
saravanakumardb1 2026-02-28 19:33:22 -08:00
parent 711621e3d9
commit a97b730a89
2 changed files with 202 additions and 1 deletions

View File

@ -0,0 +1,189 @@
/**
* Feature flag seeding creates default flags per product on startup.
*
* Idempotent: skips flags that already exist (matched by key + productId).
* Called from server startup after Cosmos init.
*/
import * as repo from './repository.js';
import type { FeatureFlagDoc } from './types.js';
interface FlagSeedDef {
key: string;
enabled: boolean;
description: string;
platforms: string[];
percentage: number;
}
// ─── Default flags per product ───────────────────────────────────────────────
const COMMON_FLAGS: FlagSeedDef[] = [
{
key: 'maintenance_mode',
enabled: false,
description: 'Global maintenance mode — disables non-essential features',
platforms: [],
percentage: 100,
},
{
key: 'telemetry_enabled',
enabled: true,
description: 'Client telemetry collection enabled',
platforms: [],
percentage: 100,
},
];
const PRODUCT_FLAGS: Record<string, FlagSeedDef[]> = {
chronomind: [
{
key: 'routines_enabled',
enabled: true,
description: 'Routines feature visible in navigation',
platforms: ['web', 'ios', 'android'],
percentage: 100,
},
{
key: 'focus_mode_enabled',
enabled: true,
description: 'Pomodoro / focus mode available',
platforms: ['web', 'ios', 'android'],
percentage: 100,
},
{
key: 'calendar_import_enabled',
enabled: true,
description: 'Calendar .ics import feature',
platforms: ['web'],
percentage: 100,
},
{
key: 'cloud_sync_enabled',
enabled: false,
description: 'Cloud sync for timers and routines',
platforms: [],
percentage: 0,
},
],
nomgap: [
{
key: 'ai_coach_enabled',
enabled: false,
description: 'AI coaching card on fasting screen (Pro feature)',
platforms: ['ios', 'android'],
percentage: 0,
},
{
key: 'social_fasting_enabled',
enabled: false,
description: 'Social fasting / group fasts',
platforms: ['ios', 'android'],
percentage: 0,
},
{
key: 'meal_logging_enabled',
enabled: true,
description: 'Meal logging before/after fasts',
platforms: ['ios', 'android'],
percentage: 100,
},
{
key: 'anatomical_mode_enabled',
enabled: false,
description: 'Anatomical body visualization (Pro)',
platforms: ['ios', 'android'],
percentage: 0,
},
],
mindlyst: [
{
key: 'voice_capture_enabled',
enabled: true,
description: 'Voice capture / transcription',
platforms: ['ios', 'android'],
percentage: 100,
},
{
key: 'ai_triage_enabled',
enabled: true,
description: 'AI auto-triage into brains',
platforms: ['ios', 'android', 'web'],
percentage: 100,
},
{
key: 'share_cards_enabled',
enabled: false,
description: 'Shareable memory cards',
platforms: [],
percentage: 0,
},
],
lysnrai: [
{
key: 'keyboard_dictation_enabled',
enabled: true,
description: 'Keyboard extension dictation',
platforms: ['ios'],
percentage: 100,
},
{
key: 'smart_punctuation_enabled',
enabled: true,
description: 'Smart punctuation in dictation',
platforms: ['ios', 'desktop'],
percentage: 100,
},
{
key: 'speak_to_translate_enabled',
enabled: false,
description: 'Speak-to-translate mode (requires Azure Translator)',
platforms: ['ios'],
percentage: 0,
},
],
};
// ─── Seed function ───────────────────────────────────────────────────────────
export async function seedDefaultFlags(logger?: { info: (msg: string) => void }): Promise<number> {
const log = logger ?? { info: () => {} };
let created = 0;
const products = Object.keys(PRODUCT_FLAGS);
for (const productId of products) {
const flags = [...COMMON_FLAGS, ...PRODUCT_FLAGS[productId]];
for (const def of flags) {
const existing = await repo.getByKey(def.key, productId);
if (existing) continue;
const now = new Date().toISOString();
const doc: FeatureFlagDoc = {
id: `flag_${productId}_${def.key}`,
productId,
key: def.key,
enabled: def.enabled,
description: def.description,
platforms: def.platforms,
regions: [],
osVersions: [],
segments: [],
percentage: def.percentage,
createdAt: now,
updatedAt: now,
};
await repo.create(doc);
created++;
log.info(`Seeded flag: ${productId}/${def.key} (enabled=${def.enabled})`);
}
}
if (created > 0) {
log.info(`Feature flag seeding complete: ${created} flags created`);
}
return created;
}

View File

@ -42,6 +42,10 @@ import { itemRoutes } from './modules/items/routes.js';
import { commentRoutes } from './modules/comments/routes.js';
import { voteRoutes } from './modules/votes/routes.js';
import { memoryRoutes } from './modules/memory/routes.js';
import { brainRoutes } from './modules/brains/routes.js';
import { streakRoutes } from './modules/streaks/routes.js';
import { reflectionRoutes } from './modules/reflections/routes.js';
import { dailyBriefRoutes } from './modules/daily-briefs/routes.js';
import { publicRoutes } from './modules/public/routes.js';
import { tokenRoutes } from './modules/tokens/routes.js';
import { themeRoutes } from './modules/themes/routes.js';
@ -72,10 +76,14 @@ import { changelogRoutes } from './modules/changelog/routes.js';
import { pushTriggerRoutes } from './modules/push-triggers/routes.js';
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
import { config } from './lib/config.js';
import { seedDefaultFlags } from './modules/flags/seed.js';
await initCosmosIfNeeded();
await loadProductCache();
// Seed default feature flags (idempotent, best-effort)
seedDefaultFlags({ info: (msg: string) => console.log(`[flags-seed] ${msg}`) }).catch(() => {});
const app = await createServiceApp({
name: 'platform-service',
version: '0.1.0',
@ -132,8 +140,12 @@ await app.register(settingsRoutes, { prefix: '/api' });
await app.register(itemRoutes, { prefix: '/api' });
await app.register(commentRoutes, { prefix: '/api' });
await app.register(voteRoutes, { prefix: '/api' });
// Mobile capture modules
// MindLyst modules (brains, memory, streaks, reflections, daily briefs)
await app.register(brainRoutes, { prefix: '/api' });
await app.register(memoryRoutes, { prefix: '/api' });
await app.register(streakRoutes, { prefix: '/api' });
await app.register(reflectionRoutes, { prefix: '/api' });
await app.register(dailyBriefRoutes, { prefix: '/api' });
// API tokens module
await app.register(tokenRoutes, { prefix: '/api' });
// Themes module