FTS.Platform.Templates
0.15.0
dotnet new install FTS.Platform.Templates@0.15.0
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.