Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
155 commits
Select commit Hold shift + click to select a range
e0a08ad
Add feature flag registry, domain model, database schema, and data mi…
tjementum Mar 10, 2026
fba6605
Add feature flag evaluation service, JWT claims, and telemetry integr…
tjementum Mar 10, 2026
61ba92f
Add feature flag frontend registry, useFeatureFlag hook, and migrate …
tjementum Mar 10, 2026
11b94bf
Add feature flag management API endpoints, CQRS commands, and tests
tjementum Mar 10, 2026
ab47263
Add JWT invalidation when feature flags change via FeatureFlagVersion…
tjementum Apr 3, 2026
bbbe990
Add feature flag tenants query and tenants endpoint to account SCS
tjementum Apr 3, 2026
e06a96c
Add feature flag toggles to account settings and user preferences
tjementum Apr 3, 2026
716c40a
Add back-office feature flag management UI with list and detail views
tjementum Apr 3, 2026
030f086
Fix back-office cross-SCS proxy service discovery and antiforgery
tjementum Apr 3, 2026
8fa48ea
Add back-office feature flag management E2E smoke test
tjementum Apr 3, 2026
379107f
Fix rollout percentage off-by-one and add reserved bucket 0/100 seman…
tjementum Apr 3, 2026
b3a686d
Add account settings and user preferences steps to feature flag E2E test
tjementum Apr 3, 2026
da38631
Add CreatedAt timestamp to feature flag API response
tjementum Apr 3, 2026
1889d29
Improve back-office flag detail page with search, sort, tenant ID, ti…
tjementum Apr 3, 2026
a33a661
Fix tenant override switch not updating after mutation
tjementum Apr 3, 2026
9bdb5b1
Use targeted query invalidation with await for tenant override refresh
tjementum Apr 3, 2026
662bbe8
Fix tenant ID precision loss by using strongly-typed TenantId in feat…
tjementum Apr 3, 2026
3ad6577
Update frontend types for string-serialized tenant IDs
tjementum Apr 3, 2026
d494bd6
Add comprehensive feature flag E2E test with tenant override and sett…
tjementum Apr 3, 2026
97386da
Fix disabling tenant override for A/B rollout-enabled tenants
tjementum Apr 3, 2026
756305f
Add endpoint to remove tenant feature flag override
tjementum Apr 3, 2026
06df369
Add remove override button for manual tenant overrides
tjementum Apr 3, 2026
5f5cfb7
Add user-scoped feature flag endpoints and rollout bucket info
tjementum Apr 4, 2026
5c17b9c
Add tenant ID to user flag info response
tjementum Apr 4, 2026
a95b467
Polish feature flag UI with dimmed switches, bucket sorting, collapsi…
tjementum Apr 4, 2026
d905f6c
Add user-scoped A/B example flag and subscription plan to tenant info
tjementum Apr 4, 2026
fcf25fd
Polish feature flag UI with scope icons, user empty state, plan colum…
tjementum Apr 4, 2026
2281f97
Fix GetFlagUsers to return all users with computed flag state
tjementum Apr 4, 2026
021f53c
Deduplicate bucket range logic and add plan to tenant info
tjementum Apr 4, 2026
daa40ab
Refine feature flag detail layout with 3-line metadata, scope icons i…
tjementum Apr 4, 2026
1088808
Clear disabled_at when reactivating a feature flag
tjementum Apr 4, 2026
4a51bee
Clean up unused repository methods
tjementum Apr 4, 2026
7ebdf0e
Complete feature flag UI polish with server-side user search, respons…
tjementum Apr 4, 2026
49ff8c7
Add empty states and descriptive subtitles to feature flag detail pages
tjementum Apr 4, 2026
7ee3591
Hide flag description on mobile list page for better status visibility
tjementum Apr 4, 2026
4b1c5e2
Fix feature flag E2E test to use Account flags after rename
tjementum Apr 4, 2026
5e11f2a
Fix feature flag E2E test toast assertions to match current messages
tjementum Apr 4, 2026
30f3d6b
Fix flaky tenant-switching test by handling non-deterministic invitat…
tjementum Apr 4, 2026
4d8f21d
Replace hash-based rollout buckets with van der Corput sequence
tjementum Apr 4, 2026
1ac231e
Replace rollout_bucket_sequence with COUNT(*) for bucket assignment
tjementum Apr 4, 2026
db39839
Add plan-based feature flags with evaluation at token refresh
tjementum Apr 4, 2026
a257bf0
Add search and plan grouping to plan flag detail page
tjementum Apr 5, 2026
4128caf
Fix control alignment, plan group order, and deactivation behavior
tjementum Apr 5, 2026
19e3a27
Consolidate feature flag migrations into single migration
tjementum Apr 5, 2026
6f0c07b
Rename abbreviated names to descriptive names and refactor services t…
tjementum Apr 5, 2026
bbd07c8
Add group subtitles and center 64rem layout to back-office feature fl…
tjementum May 11, 2026
db1ce71
Fix tenant override persistence and switch flicker on back-office fea…
tjementum May 11, 2026
c4ecee3
Enrich feature flag pages with rich account and user rendering and ad…
tjementum May 11, 2026
75fede0
Add paginated filter toolbars and filterable backend queries on featu…
tjementum May 11, 2026
5c1335b
Add HasOverride filter and 3-chip state toggle to feature-flag detail…
tjementum May 11, 2026
eacf9c5
Fix post-rebase test seeds, regenerate translations, and clean up ide…
tjementum May 11, 2026
6af6d0d
Update feature flag e2e test to match 3-chip single-select state filter
tjementum May 12, 2026
5ad5775
Add feature-flag definition reconciler that converges DB to C# defini…
tjementum May 13, 2026
13d1767
Delete obsolete internal-api feature-flag routes and gate back-office…
tjementum May 13, 2026
cdcff94
Add orphan delete endpoint and orphanedAt DTO fields for feature flags
tjementum May 13, 2026
69a1d6e
Add orphan badge, manual delete dialog, empty state, and hide global …
tjementum May 13, 2026
8ceb316
Replace flag-version DB pings with x-user-feature-flags header
tjementum May 13, 2026
c6d51e0
Read x-user-feature-flags header into AuthenticationProvider state fo…
tjementum May 13, 2026
f4c40e2
Emit x-user-feature-flags via YARP response transform and complete co…
tjementum May 13, 2026
ad8b2dd
Consolidate cookie-swap into YARP response transform to close race wi…
tjementum May 13, 2026
78e220f
Update e2e for x-user-feature-flags propagation
tjementum May 13, 2026
840fb38
Harden feature-flag schema and handlers with named query filters, FK …
tjementum May 13, 2026
5238281
Consolidate user feature flag header emission into AuthenticationCook…
tjementum May 13, 2026
d16610d
Verify tenant and user ownership before applying back-office feature …
tjementum May 13, 2026
74e575c
Collapse feature flag migrations and replace count-based rollout inde…
tjementum May 13, 2026
eec31b5
Consolidate feature flag labels and translations into shared UI module
tjementum May 13, 2026
df28a53
Stabilize feature flag and back-office e2e tests against fresh databases
tjementum May 13, 2026
afed5e8
Generate frontend feature flag labels from backend manifest
tjementum May 13, 2026
4567ddb
Fix tenant override disable persistence and gate admin controls on gr…
tjementum May 14, 2026
ac0d3dc
Enforce lowercase kebab-case for feature flag keys
tjementum May 14, 2026
fbc2e8b
Refactor FeatureFlagDefinition to abstract base plus sealed subtypes
tjementum May 14, 2026
a8c835a
Add FeatureFlagKey union and strict-type useFeatureFlag against delet…
tjementum May 14, 2026
19b9b36
Soft-delete feature flag definition and restore on re-add
tjementum May 14, 2026
dafd577
Document FeatureFlagDefinition properties
tjementum May 14, 2026
d47a83e
Surface orphaned and soft-deleted feature flags in BackOffice
tjementum May 14, 2026
fc79574
Add account-overview feature flag gating the account dashboard
tjementum May 14, 2026
7068e4b
Require scope on every feature flag row and rename migration to today
tjementum May 14, 2026
45b3547
Keep account dashboard route always reachable and align e2e heading e…
tjementum May 14, 2026
1bb631e
Tighten stable-feature kill-switch copy on the BackOffice flag detail…
tjementum May 14, 2026
9bcb87d
Always expose the activate toggle on tenant and user flag detail pages
tjementum May 14, 2026
59368d3
Mark stable modules as always on and refine BackOffice flag overview
tjementum May 14, 2026
203f53a
Refine BackOffice feature flag UX and fail reconcile on deleted-key c…
tjementum May 14, 2026
42d352d
Show A/B icon and hide bucket for non-A/B flags, fix plan-gated tenan…
tjementum May 14, 2026
f37eb41
Refine BackOffice feature flag UX with smarter override toggle and in…
tjementum May 14, 2026
160e45b
Drop state chips on plan feature flag page and pre-select required plans
tjementum May 14, 2026
fc42f2c
Add feature flag rollouts admin: pin entities first/last, sortable ta…
tjementum May 14, 2026
115d0d8
Fix double scrollbar from pagination ellipsis and refine feature flag…
tjementum May 14, 2026
3ded466
Build feature flag registry via reflection over static fields
tjementum May 14, 2026
19ec0dc
Remove custom-branding feature flag
tjementum May 14, 2026
efa99ed
Hide audience sections on orphaned feature flag detail page
tjementum May 14, 2026
ec1107e
Document feature flag system in README and AI rules
tjementum May 14, 2026
cb4adb5
Check in generated feature flag artifacts so CI builds @repo/ui witho…
tjementum May 14, 2026
432ad5b
Warn against trusting Aspire MCP for the local port
tjementum May 15, 2026
c5f7b47
Address review findings on feature flag system
tjementum May 15, 2026
c50c18f
Eliminate flaky e2e, dedupe SortableHead, harden parent-dep test
tjementum May 15, 2026
a72a7d6
Extract shared FeatureFlagAudienceToolbar to dedupe tenant and user t…
tjementum May 15, 2026
bf3e6b3
Fix reconciler reactivation, gate kill-switch toggles, repair antifor…
tjementum May 15, 2026
7ba5f5d
Serialize plan-flag insert and map unique violations to 409
tjementum May 15, 2026
335f92f
Share feature flag listener registry across federation boundary
tjementum May 15, 2026
e20c5aa
Add IsStableModule to feature flag manifest and reflection-based cont…
tjementum May 15, 2026
eb1bcb6
Emit stable flag key in self-service toggle telemetry
tjementum May 15, 2026
3c7a189
Converge e2e rollout cleanup to idempotent 100 to remove cross-projec…
tjementum May 15, 2026
6bbc03d
Gate bulk feature flag query rollout evaluation on base row active state
tjementum May 15, 2026
29f6bcb
Cascade delete feature flag rows when a tenant is deleted
tjementum May 15, 2026
2632635
Guard /account route with account-overview feature flag
tjementum May 15, 2026
54208ad
Emit structured telemetry for feature flag reconciler state transitions
tjementum May 15, 2026
badd37c
Emit telemetry events when plan tier toggles feature flag overrides
tjementum May 15, 2026
0dc74d8
Validate TelemetryName uniqueness across feature flag registry
tjementum May 15, 2026
1d1528f
Modulo rollout-bucket sequence cast and drop dead useFeatureFlag isLo…
tjementum May 15, 2026
fcdbb88
Include previous percentage in FeatureFlagRolloutPercentageUpdated event
tjementum May 15, 2026
d772122
Include previous pin in AbInclusionPin update telemetry events
tjementum May 15, 2026
d9b0d16
Record feature flag row count in TenantDeleted telemetry event
tjementum May 15, 2026
b96f717
Include override count and orphan age in FeatureFlagDeleted event
tjementum May 15, 2026
13a2d71
Stop interpolating localized flag name into feature toggle toast msgid
tjementum May 15, 2026
9d6a929
Use recursive pattern in DbUpdateException unique-violation case
tjementum May 15, 2026
deb7ad7
Restore orphaned override rows when reconciling a re-added feature flag
tjementum May 15, 2026
9f895ef
Gate back-office feature flag override and rollout endpoints behind a…
tjementum May 15, 2026
3e6c2e6
Collapse per-flag telemetry tags into single user.feature_flags dimen…
tjementum May 15, 2026
596b941
Drop unused using directive in MainDbContext
tjementum May 15, 2026
616c246
Restore SharedKernel.EntityFramework using on MainDbContext
tjementum May 15, 2026
081b8dc
Rotate refresh token on inline refresh so endpoint-triggered refresh …
tjementum May 15, 2026
54c66f9
Attribute feature flag override events to Internal, Owner, or Self tr…
tjementum May 15, 2026
1389c8d
Make AlwaysOn and NeverOn pins trump rollout in feature flag evaluation
tjementum May 15, 2026
49ebf8f
Drop stale user-scoped overrides during reconciler source transition
tjementum May 15, 2026
4111f23
Pin expected authorization policy per back-office write endpoint
tjementum May 15, 2026
03c095c
Suppress x-user-feature-flags header when endpoint-triggered refresh …
tjementum May 15, 2026
5816185
Document feature flag evaluation precedence on FeatureFlagEvaluator
tjementum May 15, 2026
584ca9c
Split feature flag guidance into ad-hoc backend and frontend rule files
tjementum May 15, 2026
70f7cc3
Hide Activate/Deactivate toggle for non-kill-switch feature flags
tjementum May 15, 2026
a65a655
Stop debounced search from re-applying stale value after Clear filters
tjementum May 15, 2026
57e1593
Centre empty state on feature-flag detail pages
tjementum May 15, 2026
11639ad
Stop debounce from re-pushing stale search after external URL change
tjementum May 15, 2026
627d399
Enable kill-switch on compact-view and account-overview
tjementum May 15, 2026
72ba2ab
Make table pagination sticky and unify Clear filters empty state on b…
tjementum May 15, 2026
29c6d69
Move feature-flag 5-minute propagation notice to subtitle and toasts
tjementum May 15, 2026
f325aff
Require trackInTelemetry and isKillSwitchEnabled on every feature fla…
tjementum May 15, 2026
89bf877
Split feature flag definitions from registry mechanism into separate …
tjementum May 15, 2026
82ecc5c
Fix failing e2e tests across feature-flag and federated navigation flows
tjementum May 15, 2026
f65e984
Document frontend codegen and Worker reconciler in FeatureFlags guida…
tjementum May 15, 2026
67bc4b8
Stop unstable onSearchChange ref from re-pushing search after Clear f…
tjementum May 15, 2026
1d7abda
Expand README with summary intro and per-feature bullets for back off…
tjementum May 15, 2026
9cff4c2
Tighten README bullets so no line wraps a small trailing fragment
tjementum May 15, 2026
bb68cc5
Refine AI rules bullet so it fits on one line and reads more naturally
tjementum May 15, 2026
5030efc
Tighten README intro, TL;DR, and feature bullets
tjementum May 15, 2026
82e43c9
Split feature-flag telemetry into per-flag scoped dimensions
tjementum May 15, 2026
67424cb
Fix removeAllTenantOverrides paginator and add afterEach cleanup to s…
tjementum May 15, 2026
ea6ebda
Apply Round 4 ultra-review e2e findings V-R4-3 and N-R4-19..26
tjementum May 15, 2026
61e85ec
Drop trailing slash in /account redirect assertion for V-R4-3
tjementum May 15, 2026
f6ffa0a
Gate back-office query mirrors on base-row IsActive to match runtime …
tjementum May 15, 2026
c795514
Add negative-path tests for Activate and Deactivate kill-switch valid…
tjementum May 15, 2026
49cce33
Route isFeatureFlagEnabled through live x-user-feature-flags state in…
tjementum May 15, 2026
49a175f
Round 4 convention and telemetry-test follow-ups (V-R4-1, V-R4-2, T-R…
tjementum May 15, 2026
ea70a71
Cover reconciler, plan-source, and tenant cascade telemetry events wi…
tjementum May 15, 2026
f4153ab
Round 4 nits: kill-switch test clarity, sequence cast mask, preferenc…
tjementum May 15, 2026
75caffb
Add *.lscache to .gitignore added after upgrading C# Dev Kit’s VS Cod…
tjementum May 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .claude/rules/backend/feature-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
description: Non-obvious behaviour of the feature flag system on the backend (declaration, evaluation, lifecycle, ownership)
---

# Feature Flags (backend)

Load this when adding, removing, or changing how a feature flag is evaluated or stored. The obvious parts (declare a field, read `executionContext.UserInfo.FeatureFlags`) are not repeated.

## Subtype Is the Contract

The `FeatureFlagDefinition` subtype on a field is not just a permissions tag — it rewires database-row ownership and validator behaviour at runtime. The subtype hierarchy replaced what used to be runtime checks (see the comment above the registry in `application/shared-kernel/SharedKernel/FeatureFlags/FeatureFlags.cs`).

- `PlanGatedTenantFlag` makes `PlanBasedFeatureFlagEvaluator` the exclusive writer of tenant overrides on every JWT refresh; the reconciler stamps `Source=Plan` on the base row and the Set/Remove validators block manual edits.
- `SystemFeatureFlag` skips the database entirely — config + frontend env var only. There is no per-tenant override.
- `TenantAbTestFlag` / `UserAbTestFlag` enable rollout buckets; `IsAbTestEligible` and `ConfigurableBy*` are mutually exclusive by construction (the disjoint subtype branches), and `BucketStart/End` are ignored if you pick a non-AB subtype.
- `TenantOwnerConfigurableFlag` requires `AdminLevel.TenantOwner` and `Scope.Tenant`; handlers check both. If those ever drift you get a silent 403.

Switching subtypes on an existing flag changes the row owner on the next reconcile — confirm that's what you want before flipping a `TenantAbTestFlag` to `PlanGatedTenantFlag`.

## `IsKillSwitchEnabled` Defaults the Row to Inactive

`isKillSwitchEnabled: true` causes the reconciler to create the base row with `EnabledAt = null` — an admin must click Activate before anyone sees the flag. `ActivateFeatureFlag` / `DeactivateFeatureFlag` only operate on kill-switch flags; default-false flags (e.g. `TenantOwnerConfigurableFlag`) are globally un-killable by design — only per-tenant overrides can turn them off.

## Soft-Delete Burns the Key

The startup reconciler marks orphaned rows (`OrphanedAt`), and re-adding the same key restores them — including any orphaned tenant/user overrides. **But** once a row is `DeletedAt`-stamped (back-office hard-delete), re-adding the same key in C# throws on startup and aborts deployment. Don't re-use a name; pick a new one.

## The Four BackOffice Query Mirrors Drift Silently

`FeatureFlagEvaluator` is the canonical runtime path. The four BackOffice query handlers (`GetUserFeatureFlags`, `GetTenantFeatureFlags`, `GetFeatureFlagUsers`, `GetFeatureFlagTenants`) each carry their own copy of `EvaluateAbRollout`, `ComputeInclusionThresholdPercentage`, and `ComputeDefaultEnabled`. They agree today by construction, not by tests — if you change evaluation math, update all five and rely on `FeatureFlagEvaluatorTests` for the canonical contract.

Known divergences worth being aware of: the mirrors do NOT consult parent-dependency, and `EvaluateOverride` in the bulk-list mirrors returns `IsEnabled=true` without checking `baseRow.IsActive`. BackOffice display can therefore report a flag as enabled while runtime evaluation excludes it.

## Disable Semantics Are Asymmetric

The four "disable this flag for this entity" paths look symmetric but aren't:

- `SetTenantFeatureFlagInternal(Enabled=false)` (admin) creates a new override row with `EnabledAt == DisabledAt` if none exists. This is required: without it, the entity would stay enabled-by-rollout. Same applies to `SetUserFeatureFlagInternal`.
- `SetTenantFeatureFlagOwner(Enabled=false)` (tenant owner) no-ops if no override exists — owners can't yank themselves out of a rollout they were never explicitly in.
- `RemoveTenantFeatureFlagOverride` (admin only) is a hard `Remove`; the row is dropped.
- `SetTenantFeatureFlagInternal(Enabled=false)` is a soft `Deactivate`; the row is kept with `DisabledAt` set.

Both removal paths emit `FeatureFlagTenantOverrideRemoved`, but they produce different `OverrideCount` results in bulk admin lists because the list counts `Source=Manual` rows — `Remove` drops them, `Deactivate` keeps them.

`SetTenantFeatureFlagOwner` is NOT a back-office mutation. It lives under `/api/account/feature-flags/{key}/tenant-override` and self-validates `Role == Owner`. Plan-gated flags are explicitly blocked at the validator level for both manual paths.

## Rollout Math Is Deterministic by Flag

`RolloutBucketHasher` is a van der Corput low-discrepancy sequence offset by a per-flag FNV-1a hash of the key. Two flags at the same percentage do NOT cover the same tenants, and ramping up never reshuffles existing members — a tenant included at 10% stays included at 20%. Don't replace this with a random or modulo strategy.

`ComputeInclusionThresholdPercentage` returns the percentage at which an entity would join the rollout, but it's special-cased for pins (`AlwaysOn` → 0, `NeverOn` → null). After the pin-trumps-rollout change, pins are unconditional, so the "joins at N%" column in BackOffice lists is meaningless for pinned rows.

## Reading vs Writing

- Handlers: read `executionContext.UserInfo.FeatureFlags` (already populated from the JWT claim). Re-querying the DB at request time means you've stepped outside the JWT-claim contract.
- The `FeatureFlagEvaluator` runs at JWT refresh, not per request. Flag changes take up to the 5-minute access-token TTL to propagate.
- Mutations that change the actor's own claim must chain `AddRefreshAuthenticationTokens()` so the gateway refreshes the JWT in-flight; without that the user waits up to 5 minutes. Plain mutations of other users' flags don't need this — they'll see it on their next refresh.

## Architecture Test Guards

Every new back-office mutation belongs in `EndpointMetadataTests.AdminPolicyBackOfficeRoutes` if admin-only, and gets a paired `_WhenNonAdminBackOfficeIdentity_ShouldReturnForbidden` test. The architecture test fails if a route's declared `RequireAuthorization` policy doesn't match the allowlist.

## Telemetry

Override events (`FeatureFlagTenantOverrideSet/Removed`, `FeatureFlagUserOverrideSet/Removed`) carry a `FeatureFlagOverrideTrigger` axis: `Internal` (back-office staff), `Owner` (tenant owner), `Self` (end-user preference). Plan-source transitions emit their own `FeatureFlagPlanOverrideActivated/Deactivated` events — they're not in the trigger enum. Per-flag telemetry tags are emitted as a single comma-separated `user.feature_flags` dimension; `feature_flag.*` is reserved by the OTel semantic-conventions registry, do not reintroduce it.
37 changes: 37 additions & 0 deletions .claude/rules/frontend/feature-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
description: Non-obvious behaviour of the feature flag system on the frontend (hook, codegen, propagation timing)
---

# Feature Flags (frontend)

Load this when gating UI on a feature flag, displaying a flag, or wondering why a toggle isn't reflecting in the SPA. The obvious parts (`useFeatureFlag(<key>)` returns `{ enabled }`) are not repeated.

## `FeatureFlagKey` Is a Type-Level Contract

`<key>` is typed against `FeatureFlagKey`, a codegen string-literal union built from `SharedKernel.FeatureFlags.FeatureFlags.cs`. Never cast a string to `FeatureFlagKey`; never accept a `string` parameter where `FeatureFlagKey` would do. Removing or renaming a flag in C# turns every stale callsite into a TS compile error after the next backend build — that compile error is the safety net.

The codegen also enforces backend-before-frontend deploy order: the frontend build cannot reference a flag the backend hasn't shipped because the union is regenerated from the C# manifest.

## The Hook Doesn't Subscribe — `AuthenticationProvider` Does

`useFeatureFlag` reads `useUserInfo().featureFlags`. The bridge that turns the `x-user-feature-flags` response header into a re-render lives in `AuthenticationProvider` and short-circuits on identical flag sets, so re-renders are cheap. **Every authenticated response is the eventing channel** — there is no push, no SSE, no polling, and there is no flag-specific TanStack query to invalidate. Don't call `queryClient.invalidateQueries` for flag changes.

## System Flags Bypass the User Path

For `Scope: "system"` flags the hook reads `import.meta.runtime_env[envVar]` and ignores `userInfo` entirely. The hook handles this transparently — just call `useFeatureFlag(<key>)` regardless of scope. There is no per-tenant or per-user override for a system flag; if you need that, the flag is the wrong subtype on the backend.

## Propagation Has a 5-Minute Floor

Flag state lags behind a back-office or self-service toggle by up to the 5-minute access-token TTL. The mutation response carries `x-user-feature-flags` only when the mutating endpoint chains `AddRefreshAuthenticationTokens()` AND the gateway's endpoint-triggered refresh succeeds. Don't optimistically update for flag-driven UI; the response is the source of truth. If the backend was transiently unavailable during the refresh, the gateway suppresses `x-user-feature-flags` rather than emit the stale claim — so a "no change visible after toggle" outcome is a possible (rare) UI state.

## Labels Are Codegen Too

Display copy lives in `@repo/ui/featureFlags/labels` (`labels.generated.ts`), sourced from each `FeatureFlagDefinition.Label` / `Description`. Don't write parallel Lingui strings for flag names in components — call `getFeatureFlagLabel(key)`. The labels participate in the shared Lingui catalog under `shared-webapp/ui/translations/locale/`; translate there, not in the per-system catalog.

## Where the Surfaces Live

- Owner self-service: `/account/settings` (Features section) — `TenantOwnerConfigurableFlag` only.
- User preferences: same area — `UserConfigurableFlag` only.
- Back-office admin: `/feature-flags/{key}` — every scope.

Orphaned and soft-deleted flags surface read-only in the back-office. They never reach `useFeatureFlag` (the codegen union drops them on the next backend build), so call sites don't need a "deleted flag" branch.
2 changes: 2 additions & 0 deletions .claude/skills/db-query/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ description: Query the local Postgres database of the active Aspire worktree via

# Database Query

**Port = `.workspace/port.txt` base + 2. Never trust Aspire MCP for the port — a common critical failure that silently runs SQL on another worktree's database.**

**Read-only. Every write needs explicit user approval for that exact statement, every time — prior approvals never carry over.**

**For destructive operations (DROP, TRUNCATE, DELETE, ALTER, or anything that loses data), take extreme care. If anything in the request is even slightly unclear about the scope, target, or intent, stop and ask for clarification before executing. Always assume the most conservative interpretation.**
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ $tf/

# Visual Studio Code
.vscode
*.lscache

# ReSharper is a .NET coding add-in
_ReSharper*/
Expand Down Expand Up @@ -392,6 +393,7 @@ FodyWeavers.xsd
**/package.g.props
**/*.generated.d.ts
**/*.generated.ts
**/*.generated.json
**/Api/swagger.json
**/lib/api/*.Api.json
**/lib/api/*.Api_*.json
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Run `build` first, then `format`, `lint`, `test` in parallel with `--no-build`.

**Slow:** Aspire restart, backend format, backend lint, end-to-end tests. **Fast:** frontend format/lint, backend test.

**Aspire**: The `aspire-restart` skill manages the AppHost - always use it; never `aspire run`, `aspire restart`, or the developer CLI's `run` command. Use the Aspire MCP `list_resources` tool to look up service URLs (or read `.workspace/port.txt` if you only need the base port). In the agentic workflow, only the Guardian agent restarts Aspire. All other agents must notify the Guardian if they need it restarted.
**Aspire**: The `aspire-restart` skill manages the AppHost - always use it; never `aspire run`, `aspire restart`, or the developer CLI's `run` command. Port = `.workspace/port.txt` base + 2. Never trust Aspire MCP for the port — a common critical failure that silently runs SQL on another worktree's database. If you need other Aspire MCP data, call `mcp__aspire__select_apphost` with the cwd path first. In the agentic workflow, only the Guardian agent restarts Aspire. All other agents must notify the Guardian if they need it restarted.

Never commit, amend, or revert without explicit user instruction each time. Commit messages: one descriptive line in imperative form, no description body.

Expand Down
37 changes: 27 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,37 +22,56 @@

Kick-start building top-tier B2B & B2C cloud SaaS products with sleek design, fully localized and accessible, vertical slice architecture, automated and fast DevOps, and top-notch security.

Ships with signup and login via Google or email one-time password, Stripe-powered subscription and payment management with plan upgrades, downgrades, and invoicing, feature flags with A/B-rollout, plan-gating, and per-user/tenant overrides, and a back-office dashboard with MRR and revenue trends, plan distribution, and tenant growth.

Built to demonstrate seamless flow: backend contracts feed a fully-typed React UI, pipelines make fully automated deployments to Azure, and a multi-agent workflow built on Claude Code's native [Agent Teams](https://code.claude.com/docs/en/agent-teams) where PlatformPlatform-expert agents collaborate to deliver complete features following the opinionated architecture. Think of it as a ready-made blueprint, not a pile of parts to assemble.

## What's inside

* **Backend** - .NET 10 and C# 14 adhering to the principles of vertical slice architecture, DDD, CQRS, and clean code
* **Backend** - .NET 10 and C# 14 following the principles of vertical slice architecture, DDD, CQRS, and clean code
* **Frontend** - React 19, TypeScript, TanStack Router & Query, ShadCN 2.0 with Base UI for accessible UI
* **CI/CD** - GitHub actions for fast passwordless deployments of docker containers and infrastructure (Bicep)
* **Infrastructure** - Cost efficient and scalable Azure PaaS services like Azure Container Apps, Azure PostgreSQL, etc.
* **Infrastructure** - Cost efficient and scalable Azure PaaS like Azure Container Apps and PostgreSQL
* **Developer CLI** - Extendable .NET CLI for DevEx - set up CI/CD is one command and a couple of questions
* **AI rules** - 30+ rules & workflows for Claude Code - sync to other editors can be enabled via `.gitignore`
* **AI rules** - 30+ rules & skills, refined and battle-tested over a year of daily use, capturing our opinionated patterns
* **Multi-agent development** - Agent Teams workflow where specialized Claude Code agents with deep PlatformPlatform expertise collaborate end-to-end

Follow our [up-to-date roadmap](https://github.com/orgs/PlatformPlatform/projects/2/views/2) with core SaaS features like SSO, monitoring, alerts, multi-region, feature flags, back office for support, etc.
Follow our [up-to-date roadmap](https://github.com/orgs/PlatformPlatform/projects/2/views/2).

Show your support for our project - give us a star on GitHub! It truly means a lot! ⭐

### Back office

Operate the platform: manage account signups, users, and logins, and monitor revenue, MRR, churn, invoices, and Stripe drift.
Operate the platform from a dedicated SPA on its own hostname, secured by Entra ID Easy Auth:

* **Dashboard** - KPI tiles for total accounts, blended MRR, all-time revenue, active users, and live sessions; trend cards for MRR, revenue, tenant growth, plan distribution, user logins; activity feeds for recent signups, payments, logins, and Stripe webhook events
* **Accounts** - Search and filter every tenant; drill into detail with owner, plan, and signup activity
* **Users** - Cross-tenant user list with role and last-seen filters; drill into per-user profile and tenant
* **Invoices** - Paginated invoice ledger across every account with Stripe drift detection so finance can reconcile what's in Stripe vs. what landed in the database
* **Billing events** - Authoritative event log of subscription, payment, and billing transitions, filterable by event type and account, with deep-link from dashboard cards
* **Feature flags** - Four levers (System / Subscription plan / Account / User) plus A/B-rollout with rich telemetry; declared in C# and surfaced as a strongly-typed React hook; back-office UI for rollouts and overrides

<img src="https://platformplatformgithub.blob.core.windows.net/BackOffice.gif" alt="PlatformPlatform Back Office" title="PlatformPlatform Back Office" />

### Product demo
### User-facing SaaS product

End-user flows: tenant signup, account settings, Google login, welcome flow, accessibility and localization, and Stripe-powered subscription signup and management.
Production-ready end-user surfaces — fully localized, accessible, and ready to brand as your own product:

* **Signup** - Tenant signup with email one-time password or Google OAuth (OpenID Connect with PKCE)
* **Login** - Same OTP and Google sign-in flows, with `UNLOCK` shortcut on localhost so dev mail is optional
* **Welcome** - First-run guided flow for naming the account, uploading a logo, and inviting colleagues
* **Account settings** - Owner-editable account name, logo, and danger-zone account deletion
* **User management** - Invite users, change roles (Owner/Admin/Member), bulk delete, and recycle-bin restore
* **Subscription & billing** - Embedded Stripe Checkout & Payment Element, prorated upgrades/downgrades, billing-info editing, scheduled-downgrade banner, dunning, and payment history with invoices and credit notes
* **User profile** - Personal profile with avatar upload (Gravatar fallback), first/last name, email, and job title
* **User preferences** - Theme (light/dark/system) and zoom per device, language per profile
* **Sessions** - Active session list with device type, browser, and OS, plus one-click revocation of any session

<img src="https://platformplatformgithub.blob.core.windows.net/$root/PlatformPlatformDemo.gif" alt="PlatformPlatform Demo" title="PlatformPlatform Demo" />

# Getting Started

TL;DR: Open the [PlatformPlatform](./application/PlatformPlatform.slnx) solution in Rider or Visual Studio and run the [Aspire AppHost](./application/AppHost/AppHost.csproj) project.
TL;DR: Requires .NET 10, Node, and Docker. Clone the repo and `dotnet run` from `developer-cli/` to start Aspire.

### Prerequisites

Expand Down Expand Up @@ -283,8 +302,6 @@ cd application/AppHost
dotnet run
```

Alternatively, open the [PlatformPlatform](./application/PlatformPlatform.slnx) solution in Rider or Visual Studio and run the [Aspire AppHost](./application/AppHost/AppHost.csproj) project.

On first startup, Aspire will prompt for `stripe-enabled` -- enter `true` to configure Stripe integration (see the optional Stripe setup section below) or `false` to skip.

Once the Aspire dashboard fully loads, click to the WebApp and sign up for a new account (https://app.dev.localhost:9000/signup). A one-time password (OTP) will be sent to the development mail server, but for local development, you can always use the code `UNLOCK` instead of checking the mail server.
Expand Down
Loading
Loading