Skip to content

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 — add FeatureFlag model
  • 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 to false when 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:

  • isFeatureEnabled returns false for unknown keys, never throws.
  • Admin toggle via setFeatureFlag is 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 (returns Boolean! only) so we never leak admin metadata to clients.
  • Admin mutations write a UserActivityLog entry with action: "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 — add FEATURE_FLAG query
  • Edit: castyou-frontend/packages/design-system/src/index.ts — re-export FeatureGate so 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 false on network error, never throws.
  • Works in both apps/app and apps/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-systemDataTable, 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 keyGatesDefault
EXPORT_PITCHEpic 18 export functionalitytrue
BOUNTY_HUNTEREpic 21 bounty hunter featurefalse
CAS_AI_CONCIERGEEpic 22 AI conciergefalse
PERSONAL_CONCIERGEEpic 23 personal conciergefalse
CASTING_SUBMISSIONEpic 14 structured auditionstrue
REEL_EDITOREpic 7 v2 standalone reel editor (exports to MediaItem)false
SYSTEM_POSTSEpic 25 CasTyou-authored feed poststrue

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_HUNTER OFF 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 — returns false while loading, returns flag value after fetch, returns false on error.
  • FeatureGate.test.tsx — renders children when on, fallback when off, no flash before resolve.

E2E (Playwright — Epic 27 fixture):

  • Admin flips EXPORT_PITCH OFF → talent profile loses the Export button on next reload.