Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

03 — User Journeys (Feature Flags Module)

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 only enabled and label (feature-flag.repository.ts:40) — the editor's description/module are not persisted on update (OQ-3). The client must not claim "saved" for those fields.
  • Missing key + enabled required: PUT /feature-flags with {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 builds Set<key>; isEnabled('x.y') is a sync lookup in FeatureFlagsCubit (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() calls findByKey first (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).