Appearance
Epic 30 — Feature Flags
Boolean kill-switches for product features, editable by admins at runtime. Lets us dark-launch, A/B gate, and disable misbehaving features without a deploy. Replaces the ad-hoc SystemConfig key/value pattern (Epic 11 BE-ADMIN-003) for the specific case of boolean feature toggles — SystemConfig stays for non-boolean settings.
BE-FLAGS-001 — FeatureFlag model + evaluation service
- [x] Implemented
Files:
- Edit:
castyou-backend/prisma/schema.prisma— addFeatureFlagmodel - Run:
pnpm prisma migrate dev --name feature_flags - Create:
castyou-backend/src/services/featureFlags/index.ts - Create:
castyou-backend/src/services/featureFlags/cache.ts— Redis-backed cache - Create:
castyou-backend/src/services/featureFlags/seed.ts— register known flag keys at boot
Schema:
prisma
model FeatureFlag {
key String @id // e.g. "EXPORT_PITCH", "BOUNTY_HUNTER"
enabled Boolean @default(false)
description String? // human-readable, shown in admin UI
updatedBy String? // admin user id of last editor
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
@@map("feature_flags")
}Service API:
isFeatureEnabled(key: string): Promise<boolean>— used everywhere in BE code. Reads from Redis (TTL 6h — flags change rarely, ~4 refreshes/day is plenty), falls back to Postgres, defaults tofalsewhen the row is missing.setFeatureFlag(key, enabled, adminId): Promise<FeatureFlag>— writes Postgres + invalidates Redis key (so admin toggles take effect immediately, not after the 6h TTL).listFeatureFlags(): Promise<FeatureFlag[]>— admin only.
Flag registry: keys are declared in a single FEATURE_FLAGS const (typed enum) so that a) callers can't typo a key, and b) the seed.ts boot step upserts a default-false row for any new key on every deploy. Removing a key requires deleting the registry entry — the seed will not delete rows, only insert missing ones (admins keep manual rows).
Acceptance criteria:
isFeatureEnabledreturnsfalsefor unknown keys, never throws.- Admin toggle via
setFeatureFlagis reflected in service callers immediately (write invalidates Redis). - A direct DB edit (bypassing the service) is picked up within ≤6 h (TTL fallback).
- Unit tests cover: unknown key → false, cache hit, cache miss + DB fallback, write-invalidates-cache.
BE-FLAGS-002 — GraphQL admin API + featureFlag(key) public query
- [x] Implemented
Files:
- Edit:
castyou-backend/src/graphql/schema/index.ts - Create:
castyou-backend/src/graphql/resolvers/featureFlags.ts
Schema:
graphql
type FeatureFlag {
key: String!
enabled: Boolean!
description: String
updatedBy: String
updatedAt: DateTime!
}
extend type Query {
featureFlag(key: String!): Boolean! # public — any logged-in user; returns enabled state only
adminFeatureFlags: [FeatureFlag!]! # admin only — full rows
}
extend type Mutation {
setFeatureFlag(key: String!, enabled: Boolean!): FeatureFlag! # admin only
updateFeatureFlagDescription(key: String!, description: String!): FeatureFlag! # admin only
}Notes:
featureFlag(key)is intentionally narrow (returnsBoolean!only) so we never leak admin metadata to clients.- Admin mutations write a
UserActivityLogentry withaction: "FEATURE_FLAG_CHANGE"and detail"<key> → <enabled>".
FE-FLAGS-001 — useFeatureFlag hook + <FeatureGate> component (app)
- [x] Implemented
Files:
- Create:
castyou-frontend/apps/app/src/hooks/useFeatureFlag.ts - Create:
castyou-frontend/apps/app/src/components/FeatureGate.tsx - Edit:
castyou-frontend/apps/app/src/graphql/queries.ts— addFEATURE_FLAGquery - Edit:
castyou-frontend/packages/design-system/src/index.ts— re-exportFeatureGateso landing can also consume it (see [[Design System Rule]] — gate component lives in DS, not duplicated per app).
API:
tsx
const enabled = useFeatureFlag('EXPORT_PITCH') // boolean, suspends until first fetch
<FeatureGate flag="BOUNTY_HUNTER" fallback={null}>
<BountyHunterTab />
</FeatureGate>Caching: Apollo cache keyed by flag key, cache-first policy, refetched only on full page reload or explicit refetch() — no window-focus polling. Net effect: a typical session sees one fetch per flag, and a long-lived tab picks up changes on next reload. Admins toggling a flag should expect ~hours of propagation across already-open clients, which matches the BE 6 h TTL.
Acceptance criteria:
- Initial render does not flash gated UI before the flag resolves (use suspense or
enabled ?? false). - Hook returns
falseon network error, never throws. - Works in both
apps/appandapps/landing.
FE-ADMIN-FLAGS-001 — Admin Feature Flags tab
- [x] Implemented
Files:
- Edit:
castyou-frontend/apps/app/src/pages/admin/AdminPage.tsx— add "Feature Flags" tab - Create:
castyou-frontend/apps/app/src/pages/admin/AdminFeatureFlagsPage.tsx - Create:
castyou-frontend/apps/app/src/hooks/useAdminFeatureFlags.ts
Description: Admin-only page (role: ADMIN). Paginated table (see [[Pagination Rule]]) listing every FeatureFlag row: key, description (inline-editable), enabled (DS Switch), updatedBy, updatedAt. Toggling the switch fires setFeatureFlag optimistically; on error, revert and show a DS Toast. Description edits commit on blur via updateFeatureFlagDescription.
Top of the page: a short legend explaining "Flags default to OFF. Unknown keys evaluated in code also return OFF." so admins know the safe-default behavior.
Components: all from @castyou/design-system — DataTable, Switch, Input, Toast, Pagination. Never duplicate locally (see [[Design System Rule]]).
BE-FLAGS-003 — Wire existing optional features behind flags
- [x] Implemented
Goal: Convert the features that are currently always-on but could plausibly need a kill-switch into flag-gated features. Initial set:
| Flag key | Gates | Default |
|---|---|---|
EXPORT_PITCH | Epic 18 export functionality | true |
BOUNTY_HUNTER | Epic 21 bounty hunter feature | false |
CAS_AI_CONCIERGE | Epic 22 AI concierge | false |
PERSONAL_CONCIERGE | Epic 23 personal concierge | false |
CASTING_SUBMISSION | Epic 14 structured auditions | true |
REEL_EDITOR | Epic 7 v2 standalone reel editor (exports to MediaItem) | false |
SYSTEM_POSTS | Epic 25 CasTyou-authored feed posts | true |
Files: for each gated feature, the entry resolver/route returns an error (or empty result for queries) when its flag is OFF, and the FE wraps the corresponding nav entry/page in <FeatureGate>.
Acceptance criteria:
- Toggling
BOUNTY_HUNTEROFF makes the route 404 on the next BE call from any new session (admin write invalidates Redis); already-open clients pick it up on next reload / within the FE cache window. - Unit test per gated resolver: flag OFF → returns the expected disabled response.
TEST-FLAGS-001 — Tests
- [x] Implemented
Backend (Vitest):
featureFlags/index.test.ts— cache hit/miss, default-false on unknown key, admin-only mutation auth, activity log entry written.- One integration test per flag wired in BE-FLAGS-003 confirming the resolver respects the flag.
Frontend (Vitest + RTL):
useFeatureFlag.test.ts— returnsfalsewhile loading, returns flag value after fetch, returnsfalseon error.FeatureGate.test.tsx— renders children when on, fallback when off, no flash before resolve.
E2E (Playwright — Epic 27 fixture):
- Admin flips
EXPORT_PITCHOFF → talent profile loses the Export button on next reload.