Skip to content

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 stays basic for 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 basic

The 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>/useSubmitForm form 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.cs uses UseSqlite; Postgres/SQL Server are documented aspirations, not yet implemented, so there's no DB-provider tiering to do)
  • The dotnet new template machinery + the _Template slices (the copy-me pattern showcase)

Pro-only (stripped from Community)

FeatureSymbolCoupling to handle when stripping
Multi-tenancyMultiTenantRemove 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.
WebhooksWebhooksSlices publish events (order.created, user.locked); guard each PublishAsync or ship a no-op IEventBus.
Background jobs (Hangfire)JobsPlumbing 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 managerAppearanceManagerThe 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)AuditRemove the AuditInterceptor + AuditFilter from the shared pipeline and the /admin/audit UI + <AuditTimeline>/<ActivityCard>.
Widget dashboardDashboardreact-grid-layout + widget registry + saved layouts + /api/dashboard. Community gets a simple welcome home.
Global search (⌘K)SearchCommand palette + ISearchProvider fan-out (providers live in slices).
In-app notifications / real-timeNotificationsSignalR bell + /notifications + hub + retention.
2FA (TOTP)TwoFactorProfile two-factor section + the second-step login path.
OAuth + account linkingExternalAuth (+ existing Google/Microsoft/GitHub)Login buttons + OAuthEndpoints + connected-accounts.
Bearer auth modeBearerConfig-flag scheme; Community is cookie-only.
Per-device sessionsSessionsSessionManager is woven into login + 2FA.
File uploads + image processingFileUploadsAvatar upload + blob store + Magick.NET (heavy native dep). Community = initials-only avatars.
CSV / Excel / PDF exportExportTabularExport + QuestPDF + DataGrid export menus + PDF invoices.
PWAPwavite-plugin-pwa + install/update prompts.
Onboarding tourTourdriver.js + data-tour anchors.
In-app changelogChangelog/changelog + the "what's new" indicator.
SQL Server providerN/A — dropped. Only SQLite is wired today; there's no multi-provider code to tier.
Demo (Sales) domainDemo (existing)Community = no demo; _Template is the showcase.

Symbol architecture

  • tier — choice parameter (basic | pro, default pro) — the baseline.
  • Each guard references a per-feature symbol (//#if (MultiTenant), …). Each is a computed symbol 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.json sources.modifiers, one block per symbol (like Demo).
  • Gotcha: the template engine processes //#if in .ts out of the box but not .tsx. A specialCustomOperations block registers the C-style conditional for **/*.tsx (additive — token replacement still applies). Keep guards in valid comment positions (imports, statements) — never a bare //#if line inside JSX (it would render as text in the live repo); instead build optional nodes into an always-declared ReactNode variable and render {variable}.
  • Gotcha: never write the substring #if/#endif/#else/#elif in comment prose in a template-processed file — the engine matches the token anywhere in a line, not just the //#if form 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 //#if guards are inert"). Verify every strip with a --tier basic scaffold + 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 below
  • default → follow --tier (Pro = on, Community = off). Leaving every flag at default reproduces 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:

FeatureRequiresWhy
DashboardAudit, Notificationsthe /activity (audit) + /me (notifications) widget data sources
Demo (Sales)Pro, Search, Export, AuditSales 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:

bash
# 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 off

Verified 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)

  1. PWA / Tour / Changelog — done; validated the whole symbol → exclude → scaffold-builds pipeline (incl. the .tsx conditional fix).
  2. 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.
  3. Search (⌘K) / Widget dashboard — done & verified. Community home = a simple welcome (no widget grid).
  4. Webhooks — done & verified. Community keeps IEventBus + registers NoopEventBus, so publishers (Users: 4 calls) are untouched; the dispatcher/deliveries/signing//admin/webhooks are stripped.
  5. Audit — done & verified (most woven-in). No AuditFilter exists; audit is interceptor + manual LogAsync. Community keeps a no-op IAuditService; the IAuditExempt + Sensitive/MarkSensitive markers (used by kept entities) are split into their own kept files; interceptor/writer/channel/read-UI stripped.
  6. Notifications / SignalR — done & verified. Excluded Features/Notifications/** + Platform/RealTime/**; guarded AddPlatformRealTime() and the sole external consumer (Comments @mention). FE: topbar bell, the _layout realtime hook, and the profile notification-settings section all guarded; /notifications + bell/icon/hook/api excluded. (No nav entry — notifications live in the topbar bell.)
  7. Advanced auth — done & verified. One AdvancedAuth symbol bundles 2FA + Sessions + OAuth + Bearer (configurator can split later). Excluded TwoFactorEndpoints/SessionEndpoints/OAuthEndpoints + Platform/Identity/SessionManager.cs + OAuthSetup.cs + FE (/two-factor page, the profile two-factor/sessions/connected sections, oauth-buttons, provider-icons). IdentitySetup.cs: guarded the Bearer scheme + AddConfiguredOAuthProviders + SessionManager registration as one block, and in ValidateSessionAsync kept the SecurityStampValidator.ValidateAsync call while guarding the SessionManager.IsValidAsync sid-check (reworded the OAuthSetup <see cref> to avoid a dangling cref). Login (Endpoints.cs) keeps the harmless RequiresTwoFactor branch (always false once 2FA is gone — PasswordSignInAsync issues the cookie, so dropping StartAsync is fine) but guards the SessionManager sessions param + StartAsync. Decision: UserSession.cs (entity + its self-contained IEntityTypeConfiguration, referencing only the kept IAuditExempt marker) stays live — simpler than a dormant-by-string table and the unused UserSessions table is harmless. FE: both login and register render <OAuthButtons> (register also owns useSearchParams/params/safeReturn solely for it — all guarded to avoid TS6133); built into a ReactNode var per the JSX-safe pattern; login's requires-2FA redirect guarded; the 3 profile sections guarded (imports + array entries).
  8. Multi-tenancy — done & verified. MultiTenant symbol strips the management surface, keeps the always-on single-tenant "default" RBAC scaffolding. Excluded: the whole Features/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 for ITenantContext/ITenantRoleService/TenancyOptions is unchanged), TenantUserRole, TenantRoleService, AppUserClaimsPrincipalFactory, TenantClaims, the ITenantScoped filter, and Tenant.cs (the catalog entity + its seeded "default" row — the single-tenant identity). Guards: Program.cs (UseTenantResolution() + TenantSeeder.SeedAsync); FE nav-config.ts (tenants entry + Building2/TENANT_PERM imports), nav.tsx (useMyTenantsmultiTenant defaults false), app-topbar.tsx (TenantSwitcher), _layout.tsx (TenantBranding), admin/_layout.tsx (TENANT_PERM in the access-gate array).
  9. Demo — done & verified. Demo is now a computed symbol (IncludeDemo && tier == "pro"): the existing //#if (Demo) guards (17 files) + the (!Demo) Sales exclude are untouched, but --tier basic is 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 from tier; making Demo computed keeps all guards stable while gaining tier-awareness). Docs updated (README, RELEASING, USER_GUIDE, CLAUDE, EDITIONS); RELEASING gains a --tier row + a Community verify command.
  10. Verify + dependency pruning — done. Pro (live repo + default scaffold): BE 0/0 + FE (29 routes) + dotnet test green (34 unit, 20 integration + 1 skip). Community (--tier basic scaffold): BE 0/0 + FE (13 routes) + dotnet test green (27 unit, 4 integration + 1 skip). BE dependency pruningNetForge.Server.csproj guards the feature-specific packages with XML-comment conditionals (#if … #endif inside 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 the Jobs symbol — gated DI + /hangfire dashboard + packages, with the Platform/Jobs folder and the Hangfire health check excluded; Jobs is 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.json can't carry comment guards (npm rejects them) and Vite tree-shakes unused libs out of the bundle, so the only cost is node_modules disk — 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.json but are excluded from the production bundle by Vite when their importing source is stripped. A future post-action could trim package.json if the node_modules size matters.

Edition CLI quick reference

CommandResult
dotnet new netforge -n AppPro, with demo (default).
dotnet new netforge -n App --IncludeDemo falsePro, no Sales demo (platform layer stays).
dotnet new netforge -n App --tier basicCommunity (lean MIT core); demo + all premium subsystems stripped.

Released under the MIT License.