01 — Product Overview (Feature Flags Module)
- 1. Purpose
- 2. Business goals
- 3. User goals
- 4. Stakeholders
- 5. Why this exists
- 6. Dependencies
- 7. Success metrics
- 8. Edge cases (client-relevant)
- 9. Assumptions (module)
- 10. Open questions (module-level; global ledger in 00-shared/12)
- 11. Glossary (this module)
StudyLyon — multi-tenant ERP / School Management API. This package designs the Feature Flags module client (Flutter, forward-looking spec) against the implemented NestJS backend. Every endpoint, DTO field, schema, permission, and wire contract below is derived directly from
src/modules/feature-flags/**,src/modules/organizations/**,src/modules/rbac/permissions.constants.ts,src/database/base.repository.ts,src/infrastructure/bullmq/queue.constants.ts, and the studylyon-blueprint handbook. No feature is invented; gaps are flagged in Assumptions & Open Questions and marked(planned)/(forward-looking)/(proposed)per global rules.
1. Purpose
Feature Flags let a tenant (institution) turn optional platform capabilities on and off —
biometric attendance, SMS, WhatsApp channels, AI reports, payroll, inventory, and similar
channel/integration toggles (blueprint 03-Database/COLLECTIONS.md:785-801). It is the
configuration surface for "gradual rollout" and capability gating: downstream services are
supposed to read a flag before offering a feature (PLAN.md:29 — scenario 2.6
"Configure feature flags (biometric, SMS, etc.) … Feature-toggle read in downstream services").
| Responsibility | Source |
|---|---|
CRUD of tenant feature toggles (key, enabled, label, description, module) | feature-flag.schema.ts:9-22 |
| List all / by module / enabled-only flag sets | feature-flags.controller.ts:23-34 |
Keyed upsert (create-or-update, merge on enabled+label) | feature-flag.repository.ts:32-43 |
| Bulk upsert (sequential, no transaction) | feature-flags.service.ts:37-45 |
| Soft-delete of a flag | feature-flags.service.ts:47-52 |
| Evaluation primitive for downstream services | feature-flags.service.ts:22-25 isEnabled() |
| Org-level flag map (lifecycle surface) | organizations.controller.ts:74-89, organizations.service.ts:139-154 |
| RBAC permission names (declared) | permissions.constants.ts:78-80 |
There are two distinct flag surfaces in the backend, and they are not connected in code:
- Flag store — the
feature_flagscollection (feature-flag.schema.ts:7): a per-tenant catalog of toggles with metadata. Served by/api/v1/feature-flags*. - Org lifecycle overlay —
organizations.metadata.featureFlags: Record<string, boolean>(organization.schema.ts:129-130), served byGET/PATCH /api/v1/organizations/:id/feature-flags.PATCHfull-replaces the map (organizations.service.ts:149-151).
FeatureFlagsService.isEnabled() reads only the flag store (feature-flags.service.ts:22-25).
The blueprint assigns "Feature flag evaluation" to the Organizations module
(blueprint 04-Modules/Organizations.md:17) — this integration does not exist yet (OQ-2).
2. Business goals
| Goal | Measure |
|---|---|
| Tenant can enable/disable channel features without a deploy | flag toggle round-trip < 1 s p95 (single Mongo upsert, no cache layer today) |
| Flags are per-tenant, never cross-tenant | every read scoped by tenantId from JWT (base.repository.ts:20-30) |
| Flags survive tenant reuse of keys | unique {tenantId, key} index (feature-flag.schema.ts:26) |
| Feature rollout can be staged per module | module field + {tenantId, module} index (feature-flag.schema.ts:22,27) |
| Downstream services gate on flags | isEnabled() primitive exists (feature-flags.service.ts:22-25); no consumer yet (OQ-1) |
| Config changes are auditable | flag toggles listed under Config audit category — (planned) (AUDITING.md:52) |
| Cache keeps flag evaluation cheap | sl:global:featureflag:{name}, poll + cache, 30 s TTL — (planned) (CACHE_ARCHITECTURE.md:15,33,48) |
3. User goals
- Org admin: browse all toggles for my school, see what's on/off per module, flip a channel (biometric, SMS, WhatsApp) and trust it applies.
- Feature configurator (admin delegate): edit labels/descriptions, batch-enable a rollout (bulk update), fix a mistake (rollback a flag).
- Platform admin: read flags across tenants during support; platform-level toggles.
- End users (teacher/staff/parent/student): never see a disabled feature — UI hides or explains it; no crashes when a flag flips mid-session.
4. Stakeholders
Platform operator (SaaS vendor — tenant tiers/plan gating (proposed)), institution admins,
downstream module owners (biometric, notifications, payments — all forward-looking), support
staff (misconfigured channels), QA + design + engineering consuming these docs.
5. Why this exists
One codebase serves thousands of institutions with different capabilities (school vs coaching
(planned) matrix, IMPLEMENTATION_PLAN.md:310-336) and different channel entitlements
(WhatsApp BSP approval, biometric hardware availability — FEATURE_ROADMAP.md:54-57). Flags
decouple code presence from feature availability without per-tenant branches. The
blueprint lists "Feature toggles / Gradual rollout / A/B testing" as the module's
responsibilities (MODULE_ARCHITECTURE.md:304-312); only the toggle CRUD exists in code
— gradual rollout and A/B testing are (planned) (OQ-6). The roadmap slots the framework in
Phase 3 (FEATURE_ROADMAP.md:42).
6. Dependencies
| Dependency | Role | Source |
|---|---|---|
Auth (JwtAuthGuard, global) | every flag endpoint requires bearer JWT | feature-flags.module.ts:12, app.module.ts:130 |
RBAC (global RbacGuard) | permission check — no flag endpoint declares @Permissions → effectively JWT-only (OQ-5) | app.module.ts:131, rbac.guard.ts:29 |
| Rate limit (global) | api tier default 100/min | rate-limit.constants.ts:6, rate-limit.guard.ts:36 |
| TenantContextService | tenant isolation + platform-admin bypass | base.repository.ts:20-30 |
Mongoose feature_flags | system of record | feature-flags.module.ts:13-15 |
Redis + BullMQ cache-rebuild | declared, not wired to flags — flag cache invalidation (planned) | queue.constants.ts:13, scheduler.service.ts:27,67 |
| Organizations module | org-level flag overlay surface | organizations.controller.ts:74-89 |
7. Success metrics
- Flag list/filter/read latency < 200 ms p95 (direct Mongo, no cache today —
CACHE_ARCHITECTURE.md:48proposes 30 s cache). - Toggle write p95 < 1 s; bulk of ≤ 20 flags applies in < 3 s (sequential loop, OQ-3).
- Zero cross-tenant flag leakage (scopedFilter enforced).
isEnabled()is consumed by ≥ 1 downstream module (biometric / notifications) — target; no consumer today (OQ-1).- No 5xx storm from the delete→re-create E11000 edge (OQ-4).
- Flag-change propagation to client ≤ 30 s (poll TTL) once cache lands.
8. Edge cases (client-relevant)
- Upsert drops
descriptionandmodule: DTO accepts them (update-feature-flag.dto.ts:13-26) butFeatureFlagRepository.upsertonly$setsenabledandlabel(feature-flag.repository.ts:40). Editing a flag's description/module silently persists nothing. PUT /feature-flagswith a new key = create (upsert);enabledis required → you cannot create a "disabled-by-default" flag without sendingenabled:false.- Bulk update is sequential, no transaction (
feature-flags.service.ts:40-44): a failing item mid-list leaves earlier writes applied. - Delete is soft: flag vanishes from every list (
base.repository.ts:20-30), but the unique{tenantId,key}index slot persists — re-creating the same key after delete can hit E11000 → 500 (OQ-4). - Org-level
PATCHfull-replaces: flags omitted from the body are lost (organizations.service.ts:149-151). - Missing key → 404
RESOURCE_NOT_FOUND("Feature flag "" not found." — feature-flags.service.ts:29). - Platform admin: reads bypass tenant scope (
base.repository.ts:21-23), but upsert callsrequireTenantId()directly (feature-flag.repository.ts:37) — a platform-admin write without a tenant context fails (OQ-5).
9. Assumptions (module)
- The client (Flutter) is forward-looking; the backend surface is complete and is the contract source.
feature-flags.read/update/deleteare the canonical permission names (permissions.constants.ts:78-80); the UI is designed so the server can start enforcing them without UI redesign (OQ-5).- The feature_flags collection has no seed data in code — tenants start with an empty
catalog; the UI must render a useful empty state and the client ships no hardcoded keys.
Default per-institution matrices are
(planned)(IMPLEMENTATION_PLAN.md:310-336). - Flag evaluation cache (30 s poll,
sl:global:featureflag:{name}) is documented in the blueprint, not implemented (CACHE_ARCHITECTURE.md:33,48); the client must therefore treat server responses as immediately authoritative and re-fetch per its own TTL. - "Gradual rollout" and "A/B testing" are blueprint responsibilities (
MODULE_ARCHITECTURE.md:310-312) with no API support — no percentage/audience fields exist on the schema (feature-flag.schema.ts:9-22).
10. Open questions (module-level; global ledger in 00-shared/12)
| # | Item | Impact |
|---|---|---|
| OQ-1 | isEnabled() has no consumers in src/ today (grep: only feature-flags.service.ts:22). Which downstream module (biometric, notifications, payments) evaluates flags first, and when? | All "disabled-feature" UX in this package |
| OQ-2 | Org lifecycle overlay (organizations.metadata.featureFlags) is disconnected from the flag store. Which surface wins? Should PATCH /organizations/:id/feature-flags merge instead of full-replace? | Affected-features map, admin confusion |
| OQ-3 | Bulk update: sequential awaits, no transaction/rollback (feature-flags.service.ts:40-44). Accept partial apply, or wrap in a transaction? | Bulk UX (which rows succeeded) |
| OQ-4 | Re-creating a soft-deleted key hits the unique {tenantId,key} index → E11000 → 500 (feature-flag.repository.ts:38-42). Permanent delete endpoint, or hard-delete in upsert, or accept 500? | Rollback journey |
| OQ-5 | feature-flags.* permissions exist (permissions.constants.ts:78-80) but no endpoint declares @Permissions(...) (rbac.guard.ts:29 passes when metadata is absent). When is enforcement added? Also: platform-admin upsert (tenantId requirement). | Route gating, admin UX |
| OQ-6 | Gradual rollout / A/B testing (MODULE_ARCHITECTURE.md:310-312): no fields, no endpoints. Planned phase? | Flag editor fields (percentage/audience) |
| OQ-7 | No WS topic for flag changes (00-shared/07 §8 topic list); no domain event (FeatureFlagChanged) in code. Realtime propagation (planned)? | Client TTL vs push |
11. Glossary (this module)
| Term | Meaning |
|---|---|
| Flag | One feature_flags doc: key, enabled, label?, description?, module? + BaseSchema fields |
| Key | Unique per tenant identifier (biometric.enabled, channels.whatsapp — examples only; keys are free-form strings, update-feature-flag.dto.ts:5-7) |
| Upsert | findOneAndUpdate(..., {upsert:true}) — insert or $set merge on enabled+label (feature-flag.repository.ts:38-42) |
| Enabled set | The list returned by GET /feature-flags/enabled — the client's gate data |
| Flag store | The feature_flags collection (module surface) |
| Org overlay | organizations.metadata.featureFlags map (lifecycle surface, full-replace semantics) |
| Soft delete | isDeleted:true; excluded from all scoped queries (base.repository.ts:20-30) |
| Envelope | {success,message,data,meta?,timestamp,requestId} (response-envelope.interceptor.ts:50-59) |