docs: feature-flag usage guide + cross-links
New docs/FEATURE-FLAGS.md captures when to add a flag, where the moving parts live, how to use useFeatureFlag from app code, the 4 states + 4 scope axes, kill-switch flow, naming conventions, and the parts we know aren't built yet (partnerSlug eval context, user-level flags, audit-log integration, server-side cache). CLAUDE.md gets a one-line convention entry under "Code conventions" so future devs notice it when grepping for code rules. NEXT-STEPS.md is updated: the feature-flag backend follow-up is now ticked done with a pointer to FEATURE-FLAGS.md for the remaining sub-tasks, and the "What landed" section reflects the real Infrastructure + Flags pages and the notification drawer.
This commit is contained in:
@@ -201,6 +201,11 @@ These choices were made deliberately after extensive license/architecture resear
|
|||||||
- **Prefer prose comments** over heavy JSDoc — explain *why*, not *what*
|
- **Prefer prose comments** over heavy JSDoc — explain *why*, not *what*
|
||||||
- **MongoDB** for portal app data (consistent with Målerportal, TurtleLootLine)
|
- **MongoDB** for portal app data (consistent with Målerportal, TurtleLootLine)
|
||||||
- **PostgreSQL** for services that require it (Authentik, OCIS)
|
- **PostgreSQL** for services that require it (Authentik, OCIS)
|
||||||
|
- **Feature flags ship through `useFeatureFlag('key')`**, NOT hardcoded
|
||||||
|
`if (env === ...)` checks. Risky / plan-gated / kill-switchable features
|
||||||
|
go behind a flag. See [`docs/FEATURE-FLAGS.md`](./docs/FEATURE-FLAGS.md)
|
||||||
|
for when to add one, how to use the composable, and the 4 states / 4 scope
|
||||||
|
axes.
|
||||||
|
|
||||||
### Production target (for reference, not deploy now)
|
### Production target (for reference, not deploy now)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# Feature flags
|
||||||
|
|
||||||
|
Dezky has a real, tenant-aware feature flag system. Use it whenever you ship
|
||||||
|
something that should roll out incrementally, be gated per plan/tenant, or
|
||||||
|
needs an instant kill switch in production. Don't push risky behavior behind
|
||||||
|
hardcoded `if (env === ...)` checks — flip a flag instead.
|
||||||
|
|
||||||
|
## When to add a flag
|
||||||
|
|
||||||
|
- The change can break things for real customers and you want a kill switch
|
||||||
|
- You want to ship to internal / friendly tenants first
|
||||||
|
- The feature is gated by plan tier (Pro/Enterprise)
|
||||||
|
- You're doing trunk-based development on a feature that takes more than
|
||||||
|
one PR to land
|
||||||
|
- Compliance-sensitive features (GDPR export, retention, audit) — kill
|
||||||
|
switch is mandatory
|
||||||
|
|
||||||
|
When you **don't** need one: pure UI tweaks, bug fixes, anything that's safe
|
||||||
|
to release to everyone at once.
|
||||||
|
|
||||||
|
## Where it lives
|
||||||
|
|
||||||
|
| Layer | Path | What it does |
|
||||||
|
|---|---|---|
|
||||||
|
| Schema + service | `services/platform-api/src/flags/` | CRUD + bulk eval (hash-based rollout) |
|
||||||
|
| Operator UI | `apps/operator/pages/flags.vue` + `components/FlagDetail.vue` | List, side panel, kill-switch, change history |
|
||||||
|
| Portal helper | `apps/portal/composables/useFeatureFlag.ts` | What you'll import from app code |
|
||||||
|
| Seed | `services/platform-api/src/seed/seed.service.ts` (`FLAG_SEEDS`) | The 10 flags created on bootstrap |
|
||||||
|
|
||||||
|
## Using a flag from app code
|
||||||
|
|
||||||
|
In the customer portal:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
const showNewInbox = useFeatureFlag('jmap_native_v2')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NewInbox v-if="showNewInbox" />
|
||||||
|
<LegacyInbox v-else />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
- One bulk eval per session — the composable shares a module-level cache.
|
||||||
|
- Fail-closed: every flag stays `false` if the eval call errors.
|
||||||
|
- The returned ref is reactive — gated UI stays hidden during the ~25ms
|
||||||
|
round-trip and appears when the answer lands.
|
||||||
|
|
||||||
|
For multi-flag panels or long-lived sessions:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { flags, ready, refresh } = useFeatureFlags()
|
||||||
|
```
|
||||||
|
|
||||||
|
The composable's tenant context comes from the signed-in user's JWT — no
|
||||||
|
slug parameter. Operator-side checks (where there's no "current tenant")
|
||||||
|
go directly through `POST /api/flags/evaluate` with an explicit
|
||||||
|
`{ tenantSlug }`.
|
||||||
|
|
||||||
|
## Adding a new flag
|
||||||
|
|
||||||
|
1. **Add to the seed list** in
|
||||||
|
`services/platform-api/src/seed/seed.service.ts → FLAG_SEEDS`. This
|
||||||
|
documents what the flag is for and ensures every environment gets it
|
||||||
|
on bootstrap. State defaults to `off` for safety.
|
||||||
|
2. **Restart platform-api** (or wait for HMR + the bootstrap hook). New
|
||||||
|
keys are upserted via `$setOnInsert` so existing operator edits
|
||||||
|
survive.
|
||||||
|
3. **Open `https://operator.dezky.local/flags`**, click the row, set
|
||||||
|
targeting/rollout, save.
|
||||||
|
4. **Reference the key** from app code via `useFeatureFlag('your_key')`.
|
||||||
|
|
||||||
|
Alternative: create the flag directly through the operator UI's
|
||||||
|
"New flag" button. The seed list is for keys that should always exist;
|
||||||
|
the UI is for ad-hoc experiments.
|
||||||
|
|
||||||
|
## The 4 states
|
||||||
|
|
||||||
|
| State | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `off` | Disabled for everyone, ignores scope. Default kill-switch state. |
|
||||||
|
| `on` | Enabled for everyone, ignores scope. |
|
||||||
|
| `targeted` | Explicit allowlist. Requires non-empty scope — empty allowlist evaluates to false ("nobody is on the list yet"). |
|
||||||
|
| `rollout` | Scope filter + deterministic hash bucket. `sha256("${tenantId}:${flagKey}") % 100 < pct`. Same tenant always gets the same answer until `pct` changes, so bumping 25→50 only flips the new slice. |
|
||||||
|
|
||||||
|
## The 4 scope axes (all optional, AND-ed when set)
|
||||||
|
|
||||||
|
- **plans** — `['pro', 'enterprise']`
|
||||||
|
- **tenantSlugs** — explicit allowlist of tenants
|
||||||
|
- **partnerSlugs** — partner-level pilots (not wired into eval context yet)
|
||||||
|
- **environments** — `['prod', 'staging']`
|
||||||
|
|
||||||
|
Empty list on an axis = "no restriction on this axis".
|
||||||
|
|
||||||
|
## Kill switch
|
||||||
|
|
||||||
|
One click in the operator UI flips a flag to `state: 'off'` + `pct: 0` and
|
||||||
|
appends a `kill-switch` history entry. Use it when something's misbehaving
|
||||||
|
in production and you need it dark immediately. Then triage at leisure.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **Keys** are snake_case, lowercase, start with a letter. Match the regex
|
||||||
|
in `CreateFlagDto`: `^[a-z][a-z0-9_]{1,62}[a-z0-9]$`.
|
||||||
|
- **One flag per intent**. Don't reuse `new_thing_v2` for unrelated
|
||||||
|
features — name them separately.
|
||||||
|
- **Delete flags** once a feature is `on` for everyone and you've removed
|
||||||
|
the legacy branch. Stale flags rot fast.
|
||||||
|
- **Don't gate auth, billing-critical, or audit-logging code** behind a
|
||||||
|
flag where `false` would silently skip security work. Flags should
|
||||||
|
pick between two correct paths, not enable correctness.
|
||||||
|
|
||||||
|
## What's not built yet
|
||||||
|
|
||||||
|
- **partnerSlug eval context** — the schema axis exists but the service
|
||||||
|
doesn't currently hydrate `ctx.partnerSlug` from the tenant doc.
|
||||||
|
Add when the first partner-gated flag actually needs it.
|
||||||
|
- **User-level flags** — eval is tenant-level only. If you need
|
||||||
|
per-individual gating (e.g. internal preview for specific staff),
|
||||||
|
combine `targeted` + a synthetic single-user tenant for now.
|
||||||
|
- **Audit log integration** — flag changes write to embedded `history`
|
||||||
|
on the flag doc, capped at 20. Switch to the real audit collection
|
||||||
|
once that exists.
|
||||||
|
- **Server-side cache** — `evaluateAll` re-reads all flags from Mongo
|
||||||
|
on every call. With ~10–50 flags this is fine; if a service ends up
|
||||||
|
evaluating per-request and flag count grows, add a small TTL cache
|
||||||
|
(~5s) in `FlagsService`.
|
||||||
+14
-5
@@ -152,11 +152,18 @@ What landed:
|
|||||||
- Audience-aware JwtAuthGuard accepts both `dezky-portal` and `dezky-operator`
|
- Audience-aware JwtAuthGuard accepts both `dezky-portal` and `dezky-operator`
|
||||||
- `Partner` schema + CRUD endpoints, `Tenant.partnerId` ref
|
- `Partner` schema + CRUD endpoints, `Tenant.partnerId` ref
|
||||||
- Tenant lifecycle (suspend / resume) gated by OperatorGuard
|
- Tenant lifecycle (suspend / resume) gated by OperatorGuard
|
||||||
|
- **Real Infrastructure live-probes** — `GET /health/platform` runs TCP +
|
||||||
|
HTTP probes against every neighbouring service; UI splits "Live" vs
|
||||||
|
"Planned" with honest status.
|
||||||
|
- **Real feature-flag system** — `Flag` schema + CRUD + bulk eval +
|
||||||
|
operator UI + `useFeatureFlag` composable in the portal. Hash-based
|
||||||
|
deterministic rollout. See [`FEATURE-FLAGS.md`](./FEATURE-FLAGS.md).
|
||||||
- Operator UI: Overview (real KPIs), Tenants (7-tab detail w/ Danger),
|
- Operator UI: Overview (real KPIs), Tenants (7-tab detail w/ Danger),
|
||||||
Partners (attach/detach), Users, Operator team. Visual-only Infrastructure,
|
Partners (attach/detach), Users, Operator team, real Infrastructure,
|
||||||
Feature flags, Audit. Placeholders for Support/Billing/Reports/Settings.
|
real Feature flags. Visual-only Audit. Placeholders for
|
||||||
|
Support/Billing/Reports/Settings.
|
||||||
- Interactions: ⌘K command palette, impersonation stub (modal + banner),
|
- Interactions: ⌘K command palette, impersonation stub (modal + banner),
|
||||||
incident modal, tweaks panel (theme/density/env)
|
incident modal, tweaks panel, **notification drawer**.
|
||||||
|
|
||||||
### Follow-ups before operator hits production
|
### Follow-ups before operator hits production
|
||||||
|
|
||||||
@@ -168,8 +175,10 @@ In rough priority order — bulk lifted from OPERATOR-PLAN.md:
|
|||||||
- [ ] **Real audit log collection** — `platform_audit` Mongo collection,
|
- [ ] **Real audit log collection** — `platform_audit` Mongo collection,
|
||||||
written by platform-api on every privileged action; stream from there
|
written by platform-api on every privileged action; stream from there
|
||||||
instead of `data/fixtures.ts`
|
instead of `data/fixtures.ts`
|
||||||
- [ ] **Feature flag backend** — `Flag` schema + per-tenant rollout state
|
- [x] **Feature flag backend** — shipped. See
|
||||||
+ a tiny flag-eval client every service imports
|
[`FEATURE-FLAGS.md`](./FEATURE-FLAGS.md). Remaining sub-tasks:
|
||||||
|
partnerSlug eval context, user-level flags, audit-log integration,
|
||||||
|
server-side cache (all called out in that doc).
|
||||||
- [ ] **Incident management backend** — `Incident` schema + paging
|
- [ ] **Incident management backend** — `Incident` schema + paging
|
||||||
(PagerDuty / OpsGenie / custom). Until then, IncidentModal is mock.
|
(PagerDuty / OpsGenie / custom). Until then, IncidentModal is mock.
|
||||||
- [ ] **Support ticket queue** — `SupportTicket` schema + email-in
|
- [ ] **Support ticket queue** — `SupportTicket` schema + email-in
|
||||||
|
|||||||
Reference in New Issue
Block a user