Skip to content

Commit ad4fc63

Browse files
authored
Add feature flag system with rollouts, plan gating, and back-office admin (#896)
### Summary & Motivation Introduce a full feature flag system spanning the backend (definitions, persistence, evaluator, reconciler), authentication plumbing (JWT refresh, cookie middleware, YARP transform), the SPA infrastructure (typed hook, header-driven cache), the back-office admin surface, and the user-facing settings panels. Flag state is computed server-side at token refresh and propagated to the SPA via the `x-user-feature-flags` response header, eliminating per-render polling and keeping evaluation consistent across refreshes, navigations, and cross-tab sessions. Two of the declared flags are load-bearing: `google-oauth` gates the OpenID Connect sign-in path and `subscriptions` toggles the Stripe billing surface. The rest (`beta-features`, `sso`, `account-overview`, `compact-view`, `experimental-ui`) are illustrative — they demonstrate each subtype and exercise the back-office UI but aren't part of the framework. Downstream products should delete the illustrative ones and declare their own. #### Definitions and registry - Flags are `public static readonly FeatureFlagDefinition` fields on `SharedKernel.FeatureFlags.FeatureFlags`. The registry is reflected from those fields at startup, so adding a flag is a one-line declaration with no manual array maintenance. Definitions live in `FeatureFlags.cs`; the reflection/validation mechanism lives in `FeatureFlagsRegistry.cs` (partial class) so the file developers edit stays free of plumbing - Sealed subtypes — `SystemFeatureFlag`, `TenantAbTestFlag`, `UserAbTestFlag`, `PlanGatedTenantFlag`, `TenantOwnerConfigurableFlag`, `UserConfigurableFlag` — enforce every cross-property invariant (scope, configurability, AB eligibility, plan tier, kill-switch, stable-module) at compile time. `trackInTelemetry` and `isKillSwitchEnabled` are required parameters on every subtype so the decisions are explicit at the call site - Every backend build runs the `GenerateFeatureFlagsManifest` MSBuild target which serializes the registry to JSON and then runs a Node script that produces `labels.generated.ts` (Lingui `t` macros for every flag's Label and Description) and `registry.generated.ts` (typed runtime registry used by `useFeatureFlag<Key>()`). The result: the SAME flag declaration becomes a strongly-typed key union on both sides — removing a flag in C# raises a TypeScript compile error in every SPA call site — and Lingui's extractor picks up the generated `t` macros so each flag's English text lands in the shared `.po` catalogue ready for translators - Keys are validated as lowercase kebab-case so they are safe to use verbatim in URLs, JWT claim payloads, telemetry property names, and frontend route params #### Schema and bucket assignment - One `feature_flags` table holds both the global base row per flag and the override rows scoped to tenants and users, deduplicated by a unique `(flag_key, tenant_id, user_id)` index with `NULLS NOT DISTINCT` - Indexes target the hot evaluator paths and a filtered partial index covers plan-driven rows - `rollout_bucket` columns are added to `tenants` and `users` and back-filled via a one-shot van der Corput sequence so even a 1% rollout is evenly distributed from day one; subsequent inserts pull from dedicated Postgres sequences to remain race-safe under concurrent signups #### Evaluation - `FeatureFlagEvaluator` runs at JWT refresh and returns the keys enabled for the (tenant, user) pair. Precedence per flag: manual per-flag override > entity-global A/B inclusion pin > rollout bucket range > plan tier > default off - `RolloutBucketHasher` derives a stable per-flag starting bucket from the flag key and handles the wrap-around case (e.g. range `90..10`) - A/B inclusion pins are unconditional and trump the rollout: `AlwaysOn` forces inclusion regardless of rollout percentage, `NeverOn` forces exclusion regardless. Use them to escape-hatch a tenant or user out of (or into) a rollout decision without changing the global percentage - Topological sort enforces single-level parent dependencies so a child flag is only evaluated after its parent has been considered #### Reconciliation, soft-delete, and orphan handling - `FeatureFlagDefinitionReconciler` runs once at startup and converges the database to the C# definitions: missing base rows get inserted, rows whose key was removed from code are marked orphaned, and re-adding the same key auto-restores the base row plus any orphaned tenant/user overrides - Reconcile fails fast if a developer attempts to add a flag whose key matches a previously hard-deleted row — explicit guard against silent data resurrection - The back-office surfaces orphans, lets admins inspect previously-overridden tenants and users, and offers a manual delete that cascades to every override row #### Plan-gated flags - `PlanGatedTenantFlag` ties a flag to a `PlanTier` (Basis / Standard / Premium). A background subscription-state evaluator writes plan-driven rows on upgrade and clears them on downgrade, so feature gating survives subscription changes without the customer-facing app having to know about feature flags - Manual overrides on a plan-gated flag are honored and tagged separately so admins can distinguish a deliberate grant from a plan-driven entitlement #### Authentication and header propagation - `AuthenticationCookieMiddleware` and the YARP response transform were merged so cookie refresh and `x-user-feature-flags` emission happen sequentially in one hook, eliminating the race the split design had between cookie swap and flag evaluation. The refresh token's `jti` is rotated on every inline refresh so the next endpoint-triggered refresh uses a fresh identifier - `AuthenticationProvider` (shared-webapp) reads the header from every authenticated response and pushes it into a React state slice; `useFeatureFlag` reads straight from that state instead of polling the server - When the endpoint-triggered refresh fails transiently, the gateway suppresses the `x-user-feature-flags` header rather than emit a stale claim — the SPA keeps its previous state and reconciles on the next successful refresh #### Back-office admin UI - List page with status, scope, A/B eligibility, configurability, and orphan/delete badges - Detail page with live rollout %, audience stats chips, and sortable/filterable paginated tenant and user tables backed by server-side sort and a stable tie-break by ID - Activate/Deactivate toggle is shown only for kill-switch flags so non-kill-switch flags can never be globally deactivated by accident; the toggle is hidden everywhere else (admins use overrides instead) - Single-click override toggle with optimistic state, A/B-aware color coding, and a three-click cycle that clears a redundant override when the resulting state would match the rollout default - Per-entity "Feature flag rollouts" dialog launched from the "..." menu on account and user detail pages (First / Default / Last) with a corresponding badge on the page header - Manual orphan delete dialog, read-only badge for stable modules, and the 5-minute claim refresh window surfaced in the page subtitle and as the description on every mutation toast - Every back-office write endpoint has its expected authorization policy pinned with an architecture-test guard so future changes can't silently widen access #### Self-service UI - `/account/settings` Features panel surfaces every `TenantOwnerConfigurableFlag` to tenant owners; non-owners see a read-only state - A user preferences panel surfaces `UserConfigurableFlag` instances so each user can toggle preferences independently of the tenant - Both panels share the same OpenAPI-typed mutations as the back-office, so behavior stays consistent #### Telemetry - Telemetry events fire on every flag mutation (activation, rollout %, override, A/B inclusion pin, manual delete) with snake_case property names aligned to OpenTelemetry conventions - Override events carry a `FeatureFlagOverrideTrigger` axis (`Internal` / `Owner` / `Self`) so dashboards can attribute every change to its originator. Plan-source transitions emit their own dedicated `FeatureFlagPlanOverrideActivated/Deactivated` events - `OpenTelemetryEnricher` and `ApplicationInsightsTelemetryInitializer` emit a single comma-separated `user.feature_flags` dimension carrying every `TrackInTelemetry` flag the user has enabled, so dashboards can group-by feature flag with native KQL. The `feature_flag.*` namespace is intentionally avoided because OpenTelemetry reserves it ### Checklist - [x] I have added tests, or done manual regression tests - [x] I have updated the documentation, if necessary
2 parents 1da4403 + 75caffb commit ad4fc63

225 files changed

Lines changed: 15113 additions & 437 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
description: Non-obvious behaviour of the feature flag system on the backend (declaration, evaluation, lifecycle, ownership)
3+
---
4+
5+
# Feature Flags (backend)
6+
7+
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.
8+
9+
## Subtype Is the Contract
10+
11+
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`).
12+
13+
- `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.
14+
- `SystemFeatureFlag` skips the database entirely — config + frontend env var only. There is no per-tenant override.
15+
- `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.
16+
- `TenantOwnerConfigurableFlag` requires `AdminLevel.TenantOwner` and `Scope.Tenant`; handlers check both. If those ever drift you get a silent 403.
17+
18+
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`.
19+
20+
## `IsKillSwitchEnabled` Defaults the Row to Inactive
21+
22+
`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.
23+
24+
## Soft-Delete Burns the Key
25+
26+
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.
27+
28+
## The Four BackOffice Query Mirrors Drift Silently
29+
30+
`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.
31+
32+
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.
33+
34+
## Disable Semantics Are Asymmetric
35+
36+
The four "disable this flag for this entity" paths look symmetric but aren't:
37+
38+
- `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`.
39+
- `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.
40+
- `RemoveTenantFeatureFlagOverride` (admin only) is a hard `Remove`; the row is dropped.
41+
- `SetTenantFeatureFlagInternal(Enabled=false)` is a soft `Deactivate`; the row is kept with `DisabledAt` set.
42+
43+
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.
44+
45+
`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.
46+
47+
## Rollout Math Is Deterministic by Flag
48+
49+
`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.
50+
51+
`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.
52+
53+
## Reading vs Writing
54+
55+
- 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.
56+
- The `FeatureFlagEvaluator` runs at JWT refresh, not per request. Flag changes take up to the 5-minute access-token TTL to propagate.
57+
- 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.
58+
59+
## Architecture Test Guards
60+
61+
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.
62+
63+
## Telemetry
64+
65+
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.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
description: Non-obvious behaviour of the feature flag system on the frontend (hook, codegen, propagation timing)
3+
---
4+
5+
# Feature Flags (frontend)
6+
7+
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.
8+
9+
## `FeatureFlagKey` Is a Type-Level Contract
10+
11+
`<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.
12+
13+
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.
14+
15+
## The Hook Doesn't Subscribe — `AuthenticationProvider` Does
16+
17+
`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.
18+
19+
## System Flags Bypass the User Path
20+
21+
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.
22+
23+
## Propagation Has a 5-Minute Floor
24+
25+
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.
26+
27+
## Labels Are Codegen Too
28+
29+
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.
30+
31+
## Where the Surfaces Live
32+
33+
- Owner self-service: `/account/settings` (Features section) — `TenantOwnerConfigurableFlag` only.
34+
- User preferences: same area — `UserConfigurableFlag` only.
35+
- Back-office admin: `/feature-flags/{key}` — every scope.
36+
37+
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.

.claude/skills/db-query/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ description: Query the local Postgres database of the active Aspire worktree via
55

66
# Database Query
77

8+
**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.**
9+
810
**Read-only. Every write needs explicit user approval for that exact statement, every time — prior approvals never carry over.**
911

1012
**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.**

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ $tf/
132132

133133
# Visual Studio Code
134134
.vscode
135+
*.lscache
135136

136137
# ReSharper is a .NET coding add-in
137138
_ReSharper*/
@@ -392,6 +393,7 @@ FodyWeavers.xsd
392393
**/package.g.props
393394
**/*.generated.d.ts
394395
**/*.generated.ts
396+
**/*.generated.json
395397
**/Api/swagger.json
396398
**/lib/api/*.Api.json
397399
**/lib/api/*.Api_*.json

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Run `build` first, then `format`, `lint`, `test` in parallel with `--no-build`.
1818

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

21-
**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.
21+
**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.
2222

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

README.md

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,37 +22,56 @@
2222

2323
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.
2424

25+
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.
26+
2527
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.
2628

2729
## What's inside
2830

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

37-
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.
39+
Follow our [up-to-date roadmap](https://github.com/orgs/PlatformPlatform/projects/2/views/2).
3840

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

4143
### Back office
4244

43-
Operate the platform: manage account signups, users, and logins, and monitor revenue, MRR, churn, invoices, and Stripe drift.
45+
Operate the platform from a dedicated SPA on its own hostname, secured by Entra ID Easy Auth:
46+
47+
* **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
48+
* **Accounts** - Search and filter every tenant; drill into detail with owner, plan, and signup activity
49+
* **Users** - Cross-tenant user list with role and last-seen filters; drill into per-user profile and tenant
50+
* **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
51+
* **Billing events** - Authoritative event log of subscription, payment, and billing transitions, filterable by event type and account, with deep-link from dashboard cards
52+
* **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
4453

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

47-
### Product demo
56+
### User-facing SaaS product
4857

49-
End-user flows: tenant signup, account settings, Google login, welcome flow, accessibility and localization, and Stripe-powered subscription signup and management.
58+
Production-ready end-user surfaces — fully localized, accessible, and ready to brand as your own product:
59+
60+
* **Signup** - Tenant signup with email one-time password or Google OAuth (OpenID Connect with PKCE)
61+
* **Login** - Same OTP and Google sign-in flows, with `UNLOCK` shortcut on localhost so dev mail is optional
62+
* **Welcome** - First-run guided flow for naming the account, uploading a logo, and inviting colleagues
63+
* **Account settings** - Owner-editable account name, logo, and danger-zone account deletion
64+
* **User management** - Invite users, change roles (Owner/Admin/Member), bulk delete, and recycle-bin restore
65+
* **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
66+
* **User profile** - Personal profile with avatar upload (Gravatar fallback), first/last name, email, and job title
67+
* **User preferences** - Theme (light/dark/system) and zoom per device, language per profile
68+
* **Sessions** - Active session list with device type, browser, and OS, plus one-click revocation of any session
5069

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

5372
# Getting Started
5473

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

5776
### Prerequisites
5877

@@ -283,8 +302,6 @@ cd application/AppHost
283302
dotnet run
284303
```
285304

286-
Alternatively, open the [PlatformPlatform](./application/PlatformPlatform.slnx) solution in Rider or Visual Studio and run the [Aspire AppHost](./application/AppHost/AppHost.csproj) project.
287-
288305
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.
289306

290307
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.

0 commit comments

Comments
 (0)