FTS.Platform.Templates 0.15.0

dotnet new install FTS.Platform.Templates@0.15.0
                    
This package contains a .NET Template Package you can call from the shell/command line.

FTS.Platform.Templates

Two dotnet new templates, so a new service starts already composed against the published FTS.Platform.* packages instead of being assembled by hand from a reference host.

dotnet new install FTS.Platform.Templates
dotnet new fts-api -n Ordering
dotnet new fts-module -n Billing -o src/Modules/Billing

They are deliberately thin. Everything they emit is wiring: a composition root, a nuget.config, one example module to delete, and a test project that points FTS.Platform.ArchitectureRules at the solution it just created. There are no base classes, no helper types and no conventions folder — if something here looks reusable, it belongs in a package, and finding one is a finding to report rather than a file to generate.

The two templates are versioned independently of the platform. What ties them together is --platform-version, whose default is the platform's current release.

PostgreSQL cache generation uses --db postgres --cache postgres: a dedicated cache schema/pool, explicit limits and startup initialization, CACHE-POSTGRES.md, plus schema initialization in the optional PostgreSQL Compose recipe. Other cache choices retain their existing output. The CLI-only --infrastructure single-db preset expands to PostgreSQL db/cache/audit/features/jobs/notifications; raw templates use explicit flags. Authentication, transaction effects and external delivery remain separate choices. Generation never starts Docker or implicitly upgrades an existing schema/volume.

Foundation enforcement (templates 0.13.0 / platform 0.14.0 source versions)

Identity recipes default to --identity-provider generic --auth-infra auto: generic/Clerk resolve to external, Keycloak/Supabase to local. The unpublished CLI 0.9.0/templates 0.13.0 candidate supports Keycloak/local (realm import and launch profile), Supabase/local (auth-only Compose, secure bootstrap and Development-only launch profile), and hosted Clerk (organization-bound JWT settings and a named launch profile). Explicit external generic/Keycloak and Clerk recipes generate no identity Docker setup; Supabase/external generates a strict password-session profile, SQL hook and SUPABASE.md, with no identity Docker. Clerk refuses local infrastructure, anonymous auth and fixed tenancy; generated CLERK.md explains public configuration and membership permissions. Keycloak callers wanting the former default must now pass --auth-infra external. The CLI never launches Docker. Generated AUTH.md explains login/configuration. Verify exact candidate output through both entry points, bootstrap, real PKCE login and API authorization with scripts/verify-auth-templates.ps1 -Feed <candidate-folder>.

Auth verification needs .NET, Docker, Node and the existing workspace browser-test dependencies (bun install --frozen-lockfile, bunx playwright install chromium; on Linux install browser OS dependencies too). FTS_TEST_BROWSER may point to an existing browser executable. The harness starts only its isolated test services, captures the real login redirect before contacting any local frontend, exchanges the PKCE code, and verifies the generated API. It does not generate or certify a frontend session adapter.

--cache none|memory|redis|postgres selects an optional read-cache provider; the default is none. Infrastructure read adapters can declare cache-aside or explicit bypass with CachePolicy.Strategy. Redis never starts Docker automatically. The Redis/SQLite/atomic composition includes a disabled-by-default display-label sample with transactional invalidation events; enable Cache__MutableReferenceSample=true. Durable invalidation is eventual, not immediate consistency. See the cache guide for policies, permissions, operational limits and switching providers after generation.

Endpoint profiles

Generated Module and Contracts projects load the compiler analyzer from FTS.Platform.ArchitectureRules as an analyzer/build-only dependency. dotnet build rejects layer-direction, purity and provider-boundary violations with FTS001; invalid analyzer configuration reports FTS002. The compiler and test policy share their semantic implementation. Graph, SQL ownership, test-enrollment and behavioral checks still require the generated verification script. Protect that script and CI configuration from analyzer/test suppression; compilation alone is not a full audit.

fts new api Ordering generates JWT-protected, SQLite-backed widget endpoints. A validated token must contain a separate permission claim with the exact value example.widgets.read or example.widgets.archive. These are explicit ASP.NET authorization policies, not SQL role mappings; the trusted issuer grants permissions. No development bypass, issuer credentials or usable signing key is generated. Configure the emitted Auth:* settings. Invalid configuration fails startup, including Auth:Mode=header in the protected profile.

The source candidate also supports --db postgres, including migrations, tenant-isolated stores, readiness and optional local Compose. Choose matching --audit postgres --jobs postgres with --effects atomic; PostgreSQL notifications also require PostgreSQL jobs. POSTGRES.md explains connection configuration, and POSTGRES-TESTS.md requires explicit disposable-database consent. Neither generation nor the application starts Docker. Publication remains a separate release gate.

fts add module Billing --module-id billing-core inherits the host's evaluated FtsTemplateProfile and emits billing-core.widgets.read / billing-core.widgets.archive policies. Missing/unknown host profile metadata is refused rather than interpreted as anonymous. Older hosts need an explicit security review before adding this property; changing metadata alone does not migrate existing endpoints. Direct dotnet new fts-module also defaults to protected/SQLite; use fts add module to inherit an existing host's choices instead of guessing them.

Generated protected HTTP tests exercise the real host/JWT handler and discover registered widget routes, including subsequently added modules. They distinguish 401, 403, successful writes and conflicts, reject invalid tokens, and check missing/ambiguous tenant claims when claims tenancy is selected. /health remains anonymous. The test-only signing key never reaches application settings.

Defaults: --profile protected --auth jwt --tenancy claims --db sqlite. No usable issuer configuration or development bypass is generated: configure identity before starting the app. For an anonymous process-local demonstration, explicitly select --profile demo --auth none --tenancy fixed --db none in either CLI or direct-template commands. --profile demo alone changes endpoint policy, not the independent identity/database defaults. Atomic effects remain opt-in. Reviewed provider migrations, deployment secrets, cross-platform native dependency evidence and production issuer integration still require separate work.

SQLite widget persistence

fts new api Ordering --profile protected --auth jwt --tenancy claims --db sqlite wires a scoped SQLite widget store and embeds its migrations. The store uses the Application-owned port and generic database commands; runtime modules do not reference the SQLite provider package. Every query/write uses both tenant kind and ID. Parameterized SQL and conditional updates preserve tenant boundaries and reject stale archive writes. Lazy seed insertion never resets existing archived state.

fts add module inherits evaluated FtsTemplateDatabase as well as profile/version. SQLite modules include their own store, a new 002_widgets.sql migration and database regression tests. The existing 001_initial.sql is unchanged. Older hosts must declare their actual database choice explicitly; editing metadata alone does not migrate stores. Keep module assembly/table names stable after use. The in-memory example has no historical durable rows to backfill. Existing applications/databases are not changed by this scaffold update.

Verification covers database reopening, concurrent connections, full tenant keys, parameter values, detached reads and canceled writes. scripts/verify-sqlite-restart.ps1 in this repository checks real host termination/restart via HTTP for the API example and an added module. Widget persistence does not make separate audit/event calls atomic. Platform 0.10.0 also adds full tenant identity to the audit, feature and notification providers; upgrading their historical rows requires an explicit mapping.

The SQLite provider pins native bundle 2.1.13 and generated SQLite tests assert the loaded library contains the security fix. Windows validation loads SQLite 3.53.3; other OS/RIDs still need release evidence. No SQLite audit warning is suppressed. Before upgrading an existing database, follow ADR 0013: stop all writers, back up/rehearse restore, and supply a reviewed Database__LegacyTenantKind. Mixed old/new workers are unsupported.

Atomic widget effects

fts new api Ordering --profile protected --auth jwt --tenancy claims --db sqlite --jobs sqlite --audit sqlite --effects atomic

--effects atomic selects an Application-owned commit port and a SQLite adapter that commits the widget change, required audit and module-specific versioned event job together. fts add module inherits evaluated FtsTemplateEffects; missing/unknown metadata and mismatched modules are refused. Keep --events none --notifications none: these optional after-commit ports are not the required event path, and incompatible choices fail generation.

The generated worker restores the job's tenant in a fresh scope and persists an idempotent archive receipt. Missing/duplicate required routes and missing DI dependencies fail startup. This is a local receipt example, not external delivery. A file-backed database is necessary for restart durability; SQLite memory mode remains atomic only. Receipt keys assume a terminal archive per widget ID: unarchive/recreation needs a per-operation identity. New receipt migrations preserve old migrations.

The default best-effort handler remains unchanged. Atomicity does not enable endpoint protection, configure deployment credentials, certify backups or add notification transports. See ADR 0014.

CLI 0.7.0 reads the generated fts-template.json compatibility contract before promotion. These templates support platform >=0.14.0 and <0.15.0. The CLI emits exact singleton direct package versions, restores the staged solution (including tests), and preserves the selected credential-free feed. Generated projects disable inherited central package management to retain their explicit pins. Direct dotnet new does not perform these CLI compatibility/restore checks.

These versions are development candidates, not a claim of publication. The generated API now includes scripts/verify.ps1 and a Windows/Linux GitHub Actions workflow. Run the script to test the consumer and require current-run passes from the declared architecture and runtime checks. The workflow uses GITHUB_TOKEN for package reads; grant the consumer repository access to the FTS packages or configure the read-only FTS_READ_PACKAGES_TOKEN secret. No credential is emitted into nuget.config.

Projects declare FtsProjectRole: Host, Module, Contracts, or Helper; IsTestProject identifies tests. Use dotnet new fts-module --contracts true or fts add module Inventory --contracts to emit a separate Contracts assembly with exchange models/query interface and an Application adapter. With raw dotnet new, add its Contracts project to the solution alongside implementation/tests. Its tenant argument is a trusted in-process contract, not an authorization check.

Unclassified runtime packages require evaluated metadata such as <FtsApprovedPackage Include="Dapper" Layer="Infrastructure" Reason="Reviewed parameterized persistence driver" />. Declare every resolved transitive/helper package requiring approval, not only the direct package. Approved layers limit resolved type usage; provider bans remain non-overridable. Approval is a review record, not vulnerability auditing. SDK framework packs are inventoried as frameworks.

Unknown roles fail the evaluated inventory. New modules declare their role automatically. Example is a standalone src/Modules/Example/Example.csproj, with the same evaluated policies as added modules. To remove it, remove its solution entry, the host's project reference and its directory. Also delete example-dependent ExampleDomainTests.cs, StoreTests.cs, WidgetBoundaryTests.cs and optional SqliteStoreTests.cs, and optional AtomicWidgetTests.cs, and remove the Example registration. Adapt ProtectedProfileTests.cs to the replacement routes; retain its host project reference while HTTP tests still use the host. Retain BoundaryTests.cs and the verification gate. Replace the corresponding FtsRequiredTest items with requirements for the replacement application; simply deleting tests must not make verification green. Run after restoring/building referenced projects. Single-target projects are supported; unsupported evaluation fails explicitly. The selected build configuration determines active conditional references.

The portable policy checks evaluated project/package references and Roslyn-resolved layer usage, including aliases and interpolation. API references Application, not Domain or Infrastructure. Module composition may construct its own services, while ordinary code resolves Infrastructure/handler dependencies. ArchitectureRules now has a test-only Roslyn dependency; runtime libraries and the CLI do not acquire it.

This is still a partial foundation: the protected, persistent and atomic options above do not establish complete SQL isolation or the remaining production guarantees. Older platform pins do not expose the current authentication API; use 0.14.0 with these templates. The CLI compatibility check is implemented. See the implementation plan.

Required test execution

Each generated Host/Module project declares FtsVerificationProject and FtsRequiredTest items. The gate evaluates those items with MSBuild, including imported and conditional metadata, then checks fresh TRX evidence from the exact test assembly. Deleting a suite, skipping required tests, losing a theory case or excluding its test project from the solution fails even if dotnet test exits zero. Copied results cannot increase the distinct-case count; a matching display name from another assembly does not count. All observed cases of each required method must pass.

When replacing the sample, update assertions and their required-method items together. The two host architecture methods remain mandatory. Older generated projects need explicit adoption of the metadata and both verification scripts; installing a newer CLI does not rewrite them. This is a reviewed execution contract, not tamper-proof verification or proof of assertion quality. Single-target xUnit/VSTest is the tested workflow. See ADR 0015.


fts-api — the service

A project template. dotnet new fts-api -n Ordering writes:

Ordering.slnx                             two projects, nothing else
nuget.config                              <clear/>, the fts feed, nuget.org, no credential
src/Ordering/Ordering.csproj              only the packages this composition actually uses
src/Ordering/Program.cs                   the composition root: one registration per seam
src/Modules/Example/                     the directory to delete - the layered example, six files
src/Modules/Example/ExampleModule.cs       the module's composition root
src/Modules/Example/Domain/Widget.cs       the entity that carries the rule
src/Modules/Example/Application/           ArchiveWidget (IWriteHandler) and IWidgetStore
src/Modules/Example/Infrastructure/        the selected widget store; in-memory or SQLite
src/Modules/Example/Api/                   two routes, protected or anonymous according to profile
src/Ordering/appsettings.json             the keys this composition needs, and no values it may not choose
src/Ordering/Properties/launchSettings.json  the dev-run profile: http://localhost:5080, Development
tests/Ordering.Tests/Ordering.Tests.csproj
tests/Ordering.Tests/BoundaryTests.cs     the new solution enforcing its own boundaries
tests/Ordering.Tests/ExampleDomainTests.cs  the emitted rule, in memory - deleted with Example/

-n names the solution, the host project, its namespace-free root and the test project; -o decides where they land. With no flags you get protected JWT endpoints, claims tenancy and SQLite persistence.

Parameters

The first five are five seams of ADR 0007, one flag each. The last two — --platform-version and --port — name no seam and register no provider. dotnet new fts-api --help prints every default listed here.

Parameter Choices Default What the emitted Program.cs registers
--profile demo, protected protected Protected widget endpoints require JWT identity and application permissions; demo permits anonymous access explicitly.
--permissions claims, sql claims Signed application permissions, or trusted roles resolved through an initially empty SQL permission table. SQL requires --db sqlite --auth jwt.
--auth none, jwt jwt none: AnonymousCurrentActor. jwt: validated token authentication and configuration validation. Protected requires JWT.
--db none, sqlite sqlite SQLite factory, migrations and persistent widgets; none selects the process-local example. The clock is unconditional.
--tenancy fixed, claims claims Fixed uses an explicitly configured constant tenant; claims resolves the tenant from the validated token.
--events none, inproc none none: NullEventPublisher as a Singleton — a registered IEventHandler<T> is never called. inproc: InProcessEventPublisher from FTS.Platform.Events as Scoped, since it resolves scoped handlers. This was not a flag before 0.4.0: the publisher lived in FTS.Platform.Sqlite, so --db decided it and no project could take events without a database or a database without events.
--audit none, sqlite none none: NullAuditLog as a Singleton. sqlite: SqliteAuditLog as Scoped — it takes four dependencies and two of them are Scoped under jwt, so a Singleton would capture them.
--platform-version exact version 0.14.0 The platform candidate targeted by these templates; the CLI validates compatibility and makes direct references exact singleton ranges.
--port 1–65535 5080 The loopback port in the emitted Properties/launchSettings.json. Registers nothing: no source file reads it, and a published binary never reads launchSettings.json at all. It is 5080 rather than ASP.NET Core's own 5000 because 5000 is what every other unconfigured host on the machine is already using, and a parameter rather than a constant because a constant just moves the collision to "every project scaffolded from this template". integer, so a non-numeric value is refused at scaffold time.

--profile demo --db none --auth none --tenancy fixed produces the explicit process-local demo. The host directly references Abstractions, Hosting and Defaults; its Example project references Abstractions, Application and Hosting. Neither production project needs a third-party package for this profile. Removing Example also removes its transitive Application dependency. Keep the host reference from its test project while real-host HTTP tests remain.

Protected endpoints register explicit signed permission claim policies for reads and archives. They do not require the optional SQL role-mapping provider. Demo endpoints explicitly allow anonymous access; JWT configuration alone does not turn a demo into a protected application.

Two combinations are refused, at scaffold time

Both exit 102, name the option, and write no file — rather than generating a project that fails to compile or, worse, one that starts and cannot resolve a dependency.

Refused Why
--tenancy claims without --auth jwt Claims tenancy reads the tenant from a validated token claim. Without jwt there are no claims to read.
--audit sqlite without --db sqlite SqliteAuditLog takes an IDbConnectionFactory and an IClock among its four constructor parameters, and both arrive only with the SQLite provider.

dotnet new fts-api --help prints each refusal as a parameter with an Enabled if: condition, so the rule is documented by the same lines that enforce it.

What a --auth jwt scaffold needs before it starts

AuthOptions.Resolve runs as the host's first statement and refuses, naming the missing key, rather than starting a host that would reject every token at request time. The emitted appsettings.json therefore carries the key NAMES and, for three of them, no value:

Key Emitted as Who supplies it
Auth:Mode "jwt" Pinned by the template. An absent Mode resolves to header mode in Development, and this host registered only the jwt providers.
Auth:Audience "" You. Every token is checked against it.
Auth:Authority "" You — the OIDC authority whose JWKS supplies signing keys.
Auth:TenantKind "" You. The platform ships no tenant-kind constants: that vocabulary belongs to your project, which is exactly why the template will not choose one.

For offline use, delete Auth:Authority and supply Auth:Issuer and Auth:SigningKey instead — a symmetric key of at least 32 UTF-8 bytes. Configure exactly one of Authority and SigningKey; both is ambiguous and AuthOptions.Resolve says so.

No Auth:SigningKey is emitted, blank or otherwise. This package is public, and a key-shaped literal in a public template is the one thing here that could become a real credential. Any of these keys can also arrive from the environment as Auth__Audience, Auth__Issuer, Auth__SigningKey, Auth__TenantKind, which override the file.

What a --tenancy fixed scaffold must replace

builder.Services.AddSingleton<ITenantContext>(new SingleTenantContext(new TenantRef("tenant", "default")));

Both literals are placeholders. "tenant" is the kind and "default" is the id, and they are the generic words for the slots rather than words about anything. The fixed provider takes a constant, so some pair has to be emitted; choosing a meaningful one would be this package deciding your vocabulary for you, which is the one thing the platform refuses to do anywhere else.


fts-module — a module project

An item template. dotnet new fts-module -n Billing -o src/Modules/Billing writes module sources, migrations and tests into that directory, in the layered shape ADR 0011 records:

Billing.csproj                            Abstractions + Application + Hosting, and the Migrations glob
BillingModule.cs                          the IModule implementation - the composition root
Domain/Widget.cs                          the entity that carries the rule
Application/ArchiveWidget.cs              the use case: ArchiveWidgetCommand + IWriteHandler
Application/IWidgetStore.cs               the persistence port
Infrastructure/InMemoryWidgetStore.cs     the mechanism, and the one file you replace first
Api/BillingEndpoints.cs                   two routes under /billing, secured according to profile
Migrations/001_initial.sql                one script, so the glob is matching something real
Tests/Billing.Tests.csproj                xunit.v3, and a reference to the module beside it
Tests/WidgetTests.cs                      the emitted rule, in memory: no host and no database
Tests/StoreTests.cs                       detached reads, concurrent saves, tenants and handler races
fts-template.json                        CLI/platform compatibility contract

Both templates retain an in-memory store for --db none and its focused tests. SQLite selection adds a persistent implementation of the same port. Tenant seed state is published atomically; reads return detached entities. TrySaveAsync is a compare-and-swap of the expected archive state for an existing row, not an upsert. Two handlers reading the same unarchived snapshot produce one success and one conflict; only the winner proceeds to audit and event publication. These effects remain best effort after the state change: audit failure can still leave changed state and no event (F05). Neither implementation couples these required effects to the business write; the planned business profile remains incomplete.

--db none omits the SQLite implementation and its provider-dependent tests. --db sqlite requires the host's IDbConnectionFactory; the CLI inherits that choice rather than guessing.

Parameter Default Effect
--module-id the lowercased -n name The IModule.Id. -n Billing alone yields billing.
--platform-version 0.14.0 Inherited by the CLI; explicit for direct template use.
--profile demo Inherited by the CLI; use protected for JWT permission policies.
--db none Inherited by the CLI; sqlite persists widgets.

IModule.Id is contracted stable and lowercase: it is the key AddModules registers the module under, and it lands in the module_id column of every audit row the module writes. The default is generated from the name rather than typed, so it is never the template's own word — but it is lowercased without hyphenating, so -n InventoryTracking yields inventorytracking. A multi-word id is what --module-id is for:

dotnet new fts-module -n InventoryTracking -o src/Modules/InventoryTracking --module-id inventory-tracking

Because that default is generated rather than literal, dotnet new fts-module --help prints Type: string for --module-id with no Default: line. The emitted Id is where you can see it.

It modifies no existing file

The template writes into its output directory and nowhere else. Registering the module in a host is four edits, and they are emitted as the header comment of the generated *Module.cs with your own names already substituted:

1.  dotnet sln add src/Modules/Billing/Billing.csproj
2.  dotnet sln add src/Modules/Billing/Tests/Billing.Tests.csproj
3.  dotnet add src/<host>/<host>.csproj reference src/Modules/Billing/Billing.csproj
4.  in the host's Program.cs, with no `using` at all:
    builder.Services.AddModules(builder.Configuration, new Billing.BillingModule());

The argument is namespace-qualified and there is no using, so the documented manual edit and the one fts add module makes are the same edit.

fts add module does that for you. It is still not this package's job: an item template that reached out of its output directory to edit files it did not create is a template you could not run twice, and could not read the diff of. The split is the reason the tool exists at all.

The example module fts-api emits is a directory of files in the host project, not a project — which is what keeps the none scaffold at five resolved libraries as emitted and four after the deletion. fts-module emits a project because that is the shape a module that is not an example has.


Restoring what a scaffold references

The emitted nuget.config declares two sources and no credential: the FTS feed, and nuget.org — which is required rather than optional, because FTS.Platform.Sqlite carries Microsoft.Data.Sqlite, dbup-sqlite and Dapper, FTS.Platform.Auth carries Microsoft.AspNetCore.Authentication.JwtBearer, and the test project takes xunit.v3, xunit.runner.visualstudio and Microsoft.NET.Test.Sdk. None of those is on the FTS feed. The none composition takes nothing from nuget.org at all.

GitHub Packages has no anonymous read for NuGet: even a public package needs a token. A 401 on restore means no token reached NuGet; a 403 on the download after a 200 on the index means the token lacks the read:packages scope. Keep the credential out of source control — supply it from the environment, or from a nuget.config that is not committed.

Uninstalling

dotnet new uninstall FTS.Platform.Templates
  • .NETStandard 2.0

    • No dependencies.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.15.0 0 10/5/2026
0.10.0 0 9/15/2026
0.7.0 0 9/12/2026
0.5.0 0 9/9/2026
0.3.0 0 9/9/2026
0.2.0 0 9/8/2026
0.1.0 0 9/8/2026