The tiering spec for NetForge's open-core model, and the feature catalog the configurator will draw from. One codebase: Community is the full source scaffolded with the premium template symbols off. Status: implemented & verified — all 10 build-order steps done; both editions build + test green; the per-feature configurator foundation (tri-state
--opt<X>override flags + dependency cascade) is wired into the template (see Per-feature configurator below). Next evolution: a web/CLI UI over it.
The two editions
- NetForge Community — free, MIT (scaffolded with
--tier basic; the CLI flag value staysbasicfor back-compat). A complete, beautiful single-team line-of-business starter: auth, RBAC, theming, i18n/RTL, the platform layer, and the AI-extensible vertical-slice architecture. This is the adoption funnel — it must look like a 2026 product on first run. - NetForge Pro — commercial, PolyForm Perimeter (LICENSE.md: commercial use is fine, but no reselling/republishing it as a competing template); distributed to customers/sponsors from the private master repo (
NetForge-Factory). Everything in Community plus the enterprise & advanced features below, the configurator, and support.
How it's built (no fork)
Single source of truth = the Pro repo. Community is produced by the template engine:
dotnet new netforge --tier basicThe premium slices are stripped via //#if (Symbol) comment guards + template.json sources.modifiers excludes — the exact machinery that already powers the demo on/off toggle (--IncludeDemo false). The stripped scaffold is published as the public MIT repo. There is no second codebase to maintain.
What stays in Community (the core)
- Vertical-slice backend + reflection-based feature discovery (
IFeatureEndpoints,IServiceRegistrar) - ProblemDetails + the Validation / Transaction / Performance endpoint filters
PagedRequest/PagedResult+<DataGrid>/useDataGrid+ the<Field>/useSubmitFormform layer- Standardized loading / empty / error states +
PageHeader - Settings system (single-tenant App/User scopes)
- Cookie auth: register / login / confirm / reset / profile
- RBAC: roles + permissions + the default-role-for-new-sign-ups feature
- App shell + theming (light/dark) + i18n + RTL
- Email (dev console sender, inline), rate limiting, health probes (liveness/readiness), API versioning (dormant)
- SQLite (the only DB provider currently wired —
Program.csusesUseSqlite; Postgres/SQL Server are documented aspirations, not yet implemented, so there's no DB-provider tiering to do) - The
dotnet newtemplate machinery + the_Templateslices (the copy-me pattern showcase)
Pro-only (stripped from Community)
| Feature | Symbol | Coupling to handle when stripping |
|---|---|---|
| Multi-tenancy | MultiTenant | Remove the management UI (/admin/tenants), resolution middleware, switcher, branding, invitations, multi-tenant mode. Keep the always-on single-tenant "default" scaffolding — per-tenant RBAC (TenantUserRole, the claims factory) is load-bearing. |
| Webhooks | Webhooks | Slices publish events (order.created, user.locked); guard each PublishAsync or ship a no-op IEventBus. |
| Background jobs (Hangfire) | Jobs | Plumbing for webhook delivery + notification retention + recurring jobs; the symbol gates the DI + dashboard + packages + Platform/Jobs and is forced on by Notifications/Webhooks/Demo (its consumers). Auth emails are sent inline, so they're unaffected. |
| Appearance manager | AppearanceManager | The runtime theme chooser + palette customizer (/admin/appearance + the appearance PUT + nav link). Keep the theme applier, the anonymous GET, the curated themes, and the --brandTheme/--brandColor baked at creation — Community still looks themed, it just can't be re-themed at runtime. |
| Audit (write and read) | Audit | Remove the AuditInterceptor + AuditFilter from the shared pipeline and the /admin/audit UI + <AuditTimeline>/<ActivityCard>. |
| Widget dashboard | Dashboard | react-grid-layout + widget registry + saved layouts + /api/dashboard. Community gets a simple welcome home. |
| Global search (⌘K) | Search | Command palette + ISearchProvider fan-out (providers live in slices). |
| In-app notifications / real-time | Notifications | SignalR bell + /notifications + hub + retention. |
| 2FA (TOTP) | TwoFactor | Profile two-factor section + the second-step login path. |
| OAuth + account linking | ExternalAuth (+ existing Google/Microsoft/GitHub) | Login buttons + OAuthEndpoints + connected-accounts. |
| Bearer auth mode | Bearer | Config-flag scheme; Community is cookie-only. |
| Per-device sessions | Sessions | SessionManager is woven into login + 2FA. |
| File uploads + image processing | FileUploads | Avatar upload + blob store + Magick.NET (heavy native dep). Community = initials-only avatars. |
| CSV / Excel / PDF export | Export | TabularExport + QuestPDF + DataGrid export menus + PDF invoices. |
| PWA | Pwa | vite-plugin-pwa + install/update prompts. |
| Onboarding tour | Tour | driver.js + data-tour anchors. |
| In-app changelog | Changelog | /changelog + the "what's new" indicator. |
| — | N/A — dropped. Only SQLite is wired today; there's no multi-provider code to tier. | |
| Demo (Sales) domain | Demo (existing) | Community = no demo; _Template is the showcase. |
Symbol architecture
tier— choice parameter (basic|pro, defaultpro) — the baseline.- Each guard references a per-feature symbol (
//#if (MultiTenant), …). Each is acomputedsymbol derived from a tri-state override parameter + the tier (see Per-feature configurator below). Guards stay stable while the derivation evolves — the same//#if (X)guards power both the edition tiers and the per-feature overrides with no code churn. - File-tree removals live in
template.jsonsources.modifiers, one block per symbol (likeDemo). - Gotcha: the template engine processes
//#ifin.tsout of the box but not.tsx. AspecialCustomOperationsblock registers the C-style conditional for**/*.tsx(additive — token replacement still applies). Keep guards in valid comment positions (imports, statements) — never a bare//#ifline inside JSX (it would render as text in the live repo); instead build optional nodes into an always-declaredReactNodevariable and render{variable}. - Gotcha: never write the substring
#if/#endif/#else/#elifin comment prose in a template-processed file — the engine matches the token anywhere in a line, not just the//#ifform at line start, so an embedded one opens a phantom nested conditional and strips the following lines from scaffolds. The live repo compiles (it's just a comment), so it's invisible until you build/run a scaffold; it once silently unregistered Community's audit + webhook no-op services (a comment read "the//#ifguards are inert"). Verify every strip with a--tier basicscaffold +dotnet test— the server builds clean even when a no-op DI registration is missing.
Per-feature configurator (override flags)
The tier is a baseline; every premium feature is also individually settable, so the dotnet new CLI is the configurator (a future web UI just shells out to it). Each feature X has a tri-state choice parameter --opt<X> (default | on | off) and the guard symbol is computed from it:
X = (optX == "on" || (optX == "default" && tier == "pro")) // + AND-ed dependencies, see belowdefault→ follow--tier(Pro = on, Community = off). Leaving every flag atdefaultreproduces the two editions byte-for-byte (verified: default Pro/Community build + test identically to the tier-only work).on→ force the feature in (even in Community).off→ strip it (even in Pro).
The twelve flags: --optPwa --optTour --optChangelog --optExport --optFileUploads --optSearch --optDashboard --optWebhooks --optAudit --optNotifications --optAdvancedAuth --optMultiTenant (plus the existing --IncludeDemo for the Sales demo).
Dependencies (cascade-off). A few features reference others; rather than dangle, a dependent cascades off if a dependency is off (the dependency is AND-ed into the computed symbol). Correct by construction — every one of the 2¹² combinations produces a buildable scaffold:
| Feature | Requires | Why |
|---|---|---|
| Dashboard | Audit, Notifications | the /activity (audit) + /me (notifications) widget data sources |
| Demo (Sales) | Pro, Search, Export, Audit | Sales search providers, CSV/Excel/PDF export, and the <ActivityCard> on detail pages |
Everything else is independent. (The Search↔Notifications search-provider link needs no dependency — it's excluded by either feature's modifier block.) Example:
# A lean core, but add back audit + the global search palette:
dotnet new netforge -n App --tier basic --optAudit on --optSearch on
# Full Pro, but no multi-tenancy and no file uploads:
dotnet new netforge -n App --optMultiTenant off --optFileUploads off
# Turn off audit in Pro → Dashboard + Demo cascade off automatically (they require it).
dotnet new netforge -n App --optAudit offVerified matrix (BE+FE build): basic+Audit, basic+MultiTenant, a fully-loaded Community (Audit+Notifications+Dashboard+Search+Export), Pro−Audit (cascades Dashboard+Demo), Pro−Notifications, Pro−Search, Pro−Export, Pro−Dashboard, and Pro−all-leaf-features — all green; defaults unchanged. A future web configurator surfaces this same dependency table as "requires" hints.
Build order (incremental — one verified strip per commit)
- ✅ PWA / Tour / Changelog — done; validated the whole symbol → exclude → scaffold-builds pipeline (incl. the
.tsxconditional fix). - ✅ Export / File uploads — done & verified (SQL Server dropped: only SQLite is wired). File uploads strips blob storage + Magick.NET + avatar upload; Profile shows a static initials avatar.
- ✅ Search (⌘K) / Widget dashboard — done & verified. Community home = a simple welcome (no widget grid).
- ✅ Webhooks — done & verified. Community keeps
IEventBus+ registersNoopEventBus, so publishers (Users: 4 calls) are untouched; the dispatcher/deliveries/signing//admin/webhooksare stripped. - ✅ Audit — done & verified (most woven-in). No
AuditFilterexists; audit is interceptor + manualLogAsync. Community keeps a no-opIAuditService; theIAuditExempt+Sensitive/MarkSensitivemarkers (used by kept entities) are split into their own kept files; interceptor/writer/channel/read-UI stripped. - ✅ Notifications / SignalR — done & verified. Excluded
Features/Notifications/**+Platform/RealTime/**; guardedAddPlatformRealTime()and the sole external consumer (Comments @mention). FE: topbar bell, the_layoutrealtime hook, and the profile notification-settings section all guarded;/notifications+ bell/icon/hook/api excluded. (No nav entry — notifications live in the topbar bell.) - ✅ Advanced auth — done & verified. One
AdvancedAuthsymbol bundles 2FA + Sessions + OAuth + Bearer (configurator can split later). ExcludedTwoFactorEndpoints/SessionEndpoints/OAuthEndpoints+Platform/Identity/SessionManager.cs+OAuthSetup.cs+ FE (/two-factorpage, the profile two-factor/sessions/connected sections,oauth-buttons,provider-icons).IdentitySetup.cs: guarded the Bearer scheme +AddConfiguredOAuthProviders+SessionManagerregistration as one block, and inValidateSessionAsynckept theSecurityStampValidator.ValidateAsynccall while guarding theSessionManager.IsValidAsyncsid-check (reworded the OAuthSetup<see cref>to avoid a dangling cref).Login(Endpoints.cs) keeps the harmlessRequiresTwoFactorbranch (always false once 2FA is gone —PasswordSignInAsyncissues the cookie, so droppingStartAsyncis fine) but guards theSessionManager sessionsparam +StartAsync. Decision:UserSession.cs(entity + its self-containedIEntityTypeConfiguration, referencing only the keptIAuditExemptmarker) stays live — simpler than a dormant-by-string table and the unusedUserSessionstable is harmless. FE: both login and register render<OAuthButtons>(register also ownsuseSearchParams/params/safeReturnsolely for it — all guarded to avoid TS6133); built into aReactNodevar per the JSX-safe pattern; login's requires-2FA redirect guarded; the 3 profile sections guarded (imports + array entries). - ✅ Multi-tenancy — done & verified.
MultiTenantsymbol strips the management surface, keeps the always-on single-tenant"default"RBAC scaffolding. Excluded: the wholeFeatures/Tenancy/**slice (host CRUD/members/invitations/switch/accept-invite),TenantResolutionMiddleware.cs,TenantInvitation.cs(dormant table), + FE (lib/api/tenancy.ts,hooks/use-tenancy.ts,tenant-switcher/tenant-branding,components/admin/tenant-*.tsx,pages/accept-invite/**,pages/(app)/admin/tenants/**). Kept load-bearing:TenantContext(returns"default"via the no-HttpContext fallback once the middleware is gone — DI forITenantContext/ITenantRoleService/TenancyOptionsis unchanged),TenantUserRole,TenantRoleService,AppUserClaimsPrincipalFactory,TenantClaims, theITenantScopedfilter, andTenant.cs(the catalog entity + its seeded"default"row — the single-tenant identity). Guards:Program.cs(UseTenantResolution()+TenantSeeder.SeedAsync); FEnav-config.ts(tenants entry +Building2/TENANT_PERMimports),nav.tsx(useMyTenants→multiTenantdefaultsfalse),app-topbar.tsx(TenantSwitcher),_layout.tsx(TenantBranding),admin/_layout.tsx(TENANT_PERMin the access-gate array). - ✅ Demo — done & verified.
Demois now a computed symbol(IncludeDemo && tier == "pro"): the existing//#if (Demo)guards (17 files) + the(!Demo)Sales exclude are untouched, but--tier basicis always demo-free (the Sales tree couples to premium subsystems Community strips — e.g.OrderSearchProvider : ISearchProvider, so demo-on-basic would dangle). The user opt-out parameter is renamed--Demo→--IncludeDemo(dotnet templating has no conditional parameter default, so a CLI-settable symbol can't also derive fromtier; makingDemocomputed keeps all guards stable while gaining tier-awareness). Docs updated (README, RELEASING, USER_GUIDE, CLAUDE, EDITIONS); RELEASING gains a--tierrow + a Community verify command. - ✅ Verify + dependency pruning — done. Pro (live repo + default scaffold): BE 0/0 + FE (29 routes) +
dotnet testgreen (34 unit, 20 integration + 1 skip). Community (--tier basicscaffold): BE 0/0 + FE (13 routes) +dotnet testgreen (27 unit, 4 integration + 1 skip). BE dependency pruning —NetForge.Server.csprojguards the feature-specific packages with XML-comment conditionals (#if … #endifinside an HTML<!-- -->comment, inert in the live repo since MSBuild ignores comments): Community drops Magick.NET (FileUploads), QuestPDF/ClosedXML/CsvHelper (Export), Bogus (Demo), and the three OAuth packages (AdvancedAuth && {Google|Microsoft|GitHub}). Verified Community restores + builds without them. Background jobs (Hangfire) are stripped from Community via theJobssymbol — gated DI +/hangfiredashboard + packages, with thePlatform/Jobsfolder and the Hangfire health check excluded;Jobsis forced back on whenever Notifications, Webhooks, or the demo is enabled (they enqueue jobs). Verified: a Community scaffold has zero Hangfire references and builds clean. FE deps not pruned:package.jsoncan't carry comment guards (npm rejects them) and Vite tree-shakes unused libs out of the bundle, so the only cost isnode_modulesdisk — left as-is.
Dependency footprint (Community vs Pro)
- BE (pruned): Community omits Magick.NET, QuestPDF, ClosedXML, CsvHelper, Bogus, Hangfire.AspNetCore + Hangfire.Storage.SQLite, and the Google/Microsoft/GitHub OAuth packages. Core stays: EF Core + SQLite, Identity, Serilog, Scalar, FluentValidation, API versioning, OpenAPI, SpaProxy.
- FE (not pruned; tree-shaken): unused libs (driver.js, recharts, react-grid-layout, cmdk, @microsoft/signalr, vite-plugin-pwa, …) remain in
package.jsonbut are excluded from the production bundle by Vite when their importing source is stripped. A future post-action could trimpackage.jsonif thenode_modulessize matters.
Edition CLI quick reference
| Command | Result |
|---|---|
dotnet new netforge -n App | Pro, with demo (default). |
dotnet new netforge -n App --IncludeDemo false | Pro, no Sales demo (platform layer stays). |
dotnet new netforge -n App --tier basic | Community (lean MIT core); demo + all premium subsystems stripped. |