03 — User Journeys (Feature Flags Module)
- 1. Browse flags
- 2. Toggle a flag (single)
- 3. Bulk update (rollout)
- 4. Disabled-feature experience (end user)
- 5. Flag rollback (revert a mistake)
End-to-end journeys computed from
feature-flags.controller.ts+feature-flags.service.ts
organizations.service.ts. Each journey: entry, intent, decision points, system responses, loading, failures, recovery, exit.(planned)/(forward-looking)per global rules.
1. Browse flags
entry: Settings → Feature Flags (admin); deep link studylyon://settings/feature-flags
intent: see the tenant's toggle catalog, grouped, with on/off status
sequenceDiagram
actor U as Org Admin
participant F as FlagsListScreen
participant R as FeatureFlagsRepository
participant API as GET /feature-flags
U->>F: open screen
F->>R: load()
R->>API: (Bearer JWT) no query
alt 200
API-->>R: data[] sorted by key asc (feature-flags.service.ts:11)
R-->>F: group by module (client-side)
F-->>U: groups with switch rows + counts
else 401 UNAUTHENTICATED
API-->>R: → silent refresh; fail → sessionExpired
else 5xx
F-->>U: AppErrorState + Retry
end
U->>F: tap module filter chip
F->>R: load(module) → GET /feature-flags?module=X (feature-flags.controller.ts:25-27)
- Decision points: module filter vs all; "Enabled only" toggle →
GET /feature-flags/enabled(feature-flags.controller.ts:30-34) — the same set the gating cubit uses. - Loading:
AppSkeleton(list); cached last-good shown while revalidating (00-shared/06 §3.3). - Failure covers: 401 (refresh flow), 5xx (generic + requestId), offline (cached + banner).
- Exit: tap row → flag detail; back → settings hub.
- Empty state: fresh tenant has no flags (no seeding in code) →
AppEmptyState"No feature flags yet — create one or contact your platform team."
2. Toggle a flag (single)
entry: flags list switch, or detail screen action
intent: change enabled state for one key
sequenceDiagram
actor U as Configurator
participant L as FlagsListScreen
participant R as FeatureFlagsRepository
participant API as PUT /feature-flags
U->>L: flip switch "channels.whatsapp"
L->>L: optimistic: switch on + row "saving"
L->>R: upsert({key, enabled:true, label})
R->>API: body {key, enabled, label?}
alt 200
API-->>R: flag doc (server truth)
R-->>L: reconcile row (server doc)
L-->>U: snackbar "Feature enabled" + lightImpact
else 404 RESOURCE_NOT_FOUND
API-->>R: (n/a for upsert — upsert creates)
else 400 VALIDATION_ERROR
R-->>L: rollback switch + inline error (missing key/enabled, update-feature-flag.dto.ts:5-11)
else 5xx (incl. E11000 duplicate on re-create, OQ-4)
R-->>L: rollback switch + snackbar error + requestId
end
- Optimistic per
00-shared/06 §3.5: toggles are safe mutations → apply locally, rollback on error. Reconcile with server payload on success. - Critical semantics (server): upsert
$sets onlyenabledandlabel(feature-flag.repository.ts:40) — the editor'sdescription/moduleare not persisted on update (OQ-3). The client must not claim "saved" for those fields. - Missing key +
enabledrequired:PUT /feature-flagswith{key, enabled:false}is how you create a flag disabled-by-default (update-feature-flag.dto.ts:5-11). - Propagation: no event, no cache today → server truth immediately; client gating cubit
refreshes per its own TTL (30 s server cache
(planned)CACHE_ARCHITECTURE.md:48).
3. Bulk update (rollout)
entry: flags list → "Bulk update" (configurator with feature-flags.update)
intent: apply a state across many flags (e.g., enable SMS for all channels at once)
sequenceDiagram
actor U as Configurator
participant B as BulkSheet
participant R as FeatureFlagsRepository
participant API as PUT /feature-flags/bulk
U->>B: multi-select rows → set on/off
B->>R: bulkUpdate([{key,enabled,label?}...])
R->>API: body = array of UpdateFeatureFlagDto
alt 200
API-->>R: data[] = server docs, **sequential** (feature-flags.service.ts:40-44)
B-->>U: per-row results: n applied, m failed (client compares keys)
else 400 VALIDATION_ERROR
B-->>U: field errors on offending rows; nothing applied if first item fails at validation
else 5xx mid-way
B-->>U: partial state shown (earlier items applied, later not) — server returns error envelope, no rollback
end
- Decision points: none server-side — array validated as a whole by class-validator; each
item needs
key+enabled(update-feature-flag.dto.ts). - Failure covers: partial application (documented, OQ-3) → UI must show exactly which keys applied; retry applies only the failed subset (client tracks).
- No transaction, no idempotency header support confirmed (
00-shared/12 B6) — the client does not auto-retry a failed bulk (would re-apply successes as no-ops — safe, since upsert is idempotent per key, but noisy).
4. Disabled-feature experience (end user)
entry: any gated module screen (biometric check-in, SMS/WhatsApp compose, payments channel)
intent: user hits a feature their tenant has switched off
flowchart TD
U[User opens app / module] --> C{FeatureFlagsCubit\nhas key?}
C -- enabled --> S[Feature UI renders\nnormal flow]
C -- disabled / missing --> H[Feature hidden or\nAppEmptyState/notice]
C -- offline / stale --> L[Last-good set used\n+ offline banner]
C -- in-flight action when flag flips --> R[Complete current action;\nhide entry points after]
H --> O[Optional: 'Ask your admin\nto enable <feature>' - proposed copy]
- Gating data source:
GET /feature-flags/enabled(feature-flags.controller.ts:30-34) → client buildsSet<key>;isEnabled('x.y')is a sync lookup inFeatureFlagsCubit(13_State_Management.md). - Missing flag = disabled: server semantics
flag?.enabled ?? false(feature-flags.service.ts:24) — client mirrors fail-closed. - Failure covers: stale cache (TTL window), offline (last-good), mid-session flip (next refresh boundary). The admin journeys above are the flip side of this journey — no dedicated server endpoint, all derived from the enabled set.
5. Flag rollback (revert a mistake)
entry: flag detail → "Disable", or list swipe → disable; delete → re-create (edge)
intent: undo a bad enablement quickly
sequenceDiagram
actor U as Configurator
participant D as FlagDetailScreen
participant R as FeatureFlagsRepository
participant API as PUT /feature-flags | DELETE /feature-flags/:key
U->>D: toggle off (or delete from menu)
D->>R: upsert({key, enabled:false, label}) OR remove(key)
alt toggle off (rollback path A)
API-->>D: 200 flag doc enabled:false — instant rollback
else delete (rollback path B)
API-->>D: 200 (void) — soft-delete (feature-flags.service.ts:47-52)
Note over D: flag disappears from all lists (scopedFilter isDeleted:false, base.repository.ts:20-30)
U->>D: "re-create" same key → PUT upsert
alt re-create works (no tombstone conflict)
API-->>D: 200 new doc
else E11000 duplicate key (soft-deleted doc still occupies unique {tenantId,key} index, feature-flag.schema.ts:26)
API-->>D: 500 INTERNAL_SERVER_ERROR — no recovery path in API (OQ-4)
end
end
- Recommendation: rollback = toggle off (path A), never delete — deletion is destructive to re-creation today (OQ-4).
- Second 404:
remove()callsfindByKeyfirst (feature-flags.service.ts:47-52) — the first 404 covers missing key; the second guards softDelete failure. - Exit: snackbar + row leaves list (delete) or switches state (toggle).
Abandonment & exit rules (all): back = return to flags list without persisting sheet edits; timeout = none server-side; permission denial = none today (OQ-5) but UI routes read/write affordances by permission; offline = list reads from cache, writes blocked with banner (no offline write queue defined for flags).