Use when starting a brand-new repository from nothing - the repo name is passed and the repo is empty. Scaffolds the estate's default containerized application in one pass: an Aspire AppHost composition root, a ServiceDefaults kernel with its size and no-domain guards, one service owning its database and migrating it from a hosted service, a Next.js product surface with runtime config, cookie sessions and the catch-all BFF proxy, multi-stage Dockerfiles, a Fly.io app per service, tag-driven workflows that provision a cold estate from the first tag, the repository baseline (secret scanning, one-command onboarding, central package management), and the docs set including ADRs and the LaTeX PDF track. Derives every name from the repo name and runs the verification gates before the first commit. Refuses a repo that already holds an application - that is a modernize or delivery session, not an init.
Initializing a repository from the generic template
You have been given the name of a repository that is empty, and you are going to leave it
holding the estate's default containerized application: an Aspire composition root, a
shared kernel, one service that owns its database, a Next.js product surface, a container
per service, a Fly.io topology, tag-driven CI/CD, and the documentation set — all of it
compliant with the constitution on the
first commit rather than on a later cleanup pass.
One rule outranks everything below, because breaking it is the most expensive failure mode
in this estate: read the standards, do not re-derive them. Every rule here is stated in
shorthand — "the kernel stays a kernel", "one image, N environments" — and each one names
the principle or the guide section it compresses. Scaffolding is where re-derivation is
most tempting and most costly, because a wrong default gets copied into every file the
repository will ever have.
The argument is the repository name. Everything else is derived from it (§1) or is a
default this document fixes, so that two repositories initialized a year apart come out the
same shape. What is genuinely a decision — the region, the registry, whether the system has
users — is asked once and recorded as an ADR, never guessed silently.
This is the greenfield entry point. Its siblings: PLAYBOOK.md is for a
repository that already holds code (REVIEW / MODERNIZE / RECOVER),
MASTER-PROMPT.md is the delivery session that brings a drifted
application back to the standards and leaves it live, and
docs/delivery/WORKFLOW.md is the per-ticket procedure that
takes over once this repository has its first ticket. If the repository is not empty,
this procedure is the wrong one (§2).
Confirm, before creating the first file, that you can actually open
00-REFERENCE-ARCHITECTURE.md and the
guides under docs/guides/ — because
architecture-core@architecture-standards is installed, or because this repository is
attached to the session. If you cannot, stop and say so.
The guides this procedure leans on directly, and which section owns which decision:
Load the guide for a layer before you write that layer's files, and say which ones you
loaded. A scaffold is twenty small decisions taken at once; the guides are where nineteen
of them are already made.
1. The argument
/init-generic-template <repo-name> — the repository this run initializes, as its GitHub
name: lowercase, hyphen-separated, matching ^[a-z][a-z0-9-]*$.
Without an argument, fall back to the repository you are already in — its origin
remote, or the working directory's name — and echo that name back before touching
anything. If there is no such repository, stop and ask for the name. Do not invent one
from the conversation's subject: every name below is derived from this string, and a
plausible-but-wrong system name is baked into a Fly app that has to be destroyed to be
renamed.
Everything else is derived, mechanically. Echo this table back as your first output, filled
in, so a wrong derivation is caught before it is written into forty files:
Derived
Rule
Example, from zoo-companion
System slug
the argument, unchanged
zoo-companion
.NET root namespace and solution
PascalCase of the slug, hyphens dropped
ZooCompanion
Projects
<Pascal>.AppHost, .ServiceDefaults, .Contracts, .Api, plus .Api.Tests
ZooCompanion.AppHost
Frontend workspace
web/, npm scope @<slug>
@zoo-companion/web-kit
Fly apps
<slug>-<service>-<env> — globally unique across all of Fly, not just your org (FLY-IO-DEPLOYMENT.md §4)
zoo-companion-api-dev
Fly Postgres app
<slug>-postgres — one instance, no environment suffix, databases inside it per service (P3, Fly §8)
zoo-companion-postgres
Databases
<service>db inside that instance
apidb
Container images
ghcr.io/<owner>/<slug>-<service>
ghcr.io/konradcinkusz/zoo-companion-api
Config prefix
the PascalCase namespace, for options sections
ZooCompanion__…
Three things are decisions rather than derivations, and each one is asked once and recorded
as an ADR under §9 — never defaulted silently:
primary_region. Pick the region nearest the users. The guides' examples show both
waw and fra; neither is a default, and a region is expensive to change once volumes
exist (Fly §8).
The registry. The constitution's §2 disagreement table settles this as GHCR —
portable and free at this scale — while the Fly guide's pipeline (§10) is written against
registry.fly.io. Take GHCR, deploy with flyctl deploy --image ghcr.io/…, and note in
the ADR that a private GHCR image needs a pull credential set as a Fly secret. Choosing
registry.fly.io instead is allowed and is the same one-line ADR.
Whether the system has users. If it does, identity is
konradcinkusz/authservice adopted as an
external service (§7); no user store and no token minting is ever written into this
template (P5). If it does not, say so in the ADR and skip every auth step — a login page
for a system with no accounts is scaffolding nobody asked for.
2. Preflight
The repository has to be empty. Empty means: nothing tracked beyond what a fresh GitHub
repository creates for itself — .git/, README.md, LICENSE, .gitignore. Anything
else — a solution, a package.json, a workflow, a src/ — means this repository already
has a shape, and dropping a template on top of it silently overwrites decisions somebody
made.
If it is not empty, stop and say so, and name the procedure that fits instead:
PLAYBOOK.md for a review or a modernization, or
MASTER-PROMPT.md for a delivery session that aligns a working
application and leaves it live. Do not "merge" the template into an existing tree.
Then check the toolchain and report it, because §10's gates are only as good as what you
can actually run:
Tool
Needed for
If absent
.NET SDK
build, test, the AppHost
Blocking — the solution cannot be verified
Node + pnpm
the frontend build
Blocking for §6's gate only
A container engine
the image gates in §10
Record the gate as not run; do not claim it passed
flyctl
nothing in this procedure
Not needed — §11 provisions from CI, never from a laptop (Fly §11)
gitleaks
§10's secret scan
Record it; the CI job in §8 still runs
Confirm the branch you are on and that it is not the default branch, and create one if
needed. Every file below lands in one branch and one pull request.
3. The tree this creates
One system, five projects, one frontend, three Fly apps. This is deliberately the smallest
system that exercises every principle — not a demonstration of every guide at once (§12).
<repo>/
├─ .claude/settings.json declares the marketplace + architecture-core (REPO-BASELINE §7)
├─ .editorconfig · .gitattributes · .gitignore · .dockerignore
├─ .gitleaks.toml · secrets.env.example
├─ CODEOWNERS (in .github/) · LICENSE · README.md · AGENTS.md
├─ global.json pinned SDK band + Aspire SDK
├─ Directory.Build.props TFM, nullable, warnings-as-errors, metadata
├─ Directory.Packages.props central package management — one version per package
├─ <Pascal>.sln
├─ src/
│ ├─ <Pascal>.AppHost/ P1 — the composition root, development only
│ ├─ <Pascal>.ServiceDefaults/ P2 — the shared kernel: plumbing, nothing else
│ ├─ <Pascal>.Contracts/ DTOs that cross a service boundary
│ └─ <Pascal>.Api/ the one service: owns apidb, exposes /health + /alive
│ └─ Dockerfile P6 — multi-stage, :8080, non-root
├─ web/ one pnpm workspace (FRONTEND-BFF §7)
│ └─ app/ the Next.js product surface + Dockerfile
├─ tests/
│ ├─ <Pascal>.Api.Tests/ xUnit, InMemory provider — P13's logic-bearing layer
│ └─ e2e/ Playwright, wired to CI from the first commit
├─ flyio/
│ ├─ api.fly.toml · web.fly.toml · postgres.fly.toml · authservice.fly.toml
│ ├─ SECRETS.md what is secret, where it lives, how to set it
│ └─ INFRASTRUCTURE-ANALYSIS.md topology, sizing, cost — Fly §13's four questions
├─ scripts/
│ ├─ setup.sh · setup.ps1 one-command onboarding, numbered steps
│ ├─ hooks/pre-commit · scan-secrets.sh
│ └─ README.md every variable by tier
├─ docs/
│ ├─ architecture/00-ARCHITECTURE.md references the constitution; carries the deviation register
│ ├─ adr/0000-template.md + 0001… the decisions §1 and §7 took
│ ├─ ux/UI-UX.md screens, flows, ranked backlog
│ ├─ diagrams/*.mmd one diagram per file, one source (RESEARCH §"Diagrams in a PDF")
│ └─ papers/<slug>-overview.tex house style, explicitly not a research paper
└─ .github/
├─ CODEOWNERS · dependabot.yml (declared, limit 0) · pull_request_template.md · ISSUE_TEMPLATE/
├─ agents/ in-repo agent definitions, allowlisted tools
└─ workflows/ ci.yml · secret-scan.yml · codeql.yml
flyio.yml · flyio-scale.yml · flyio-destroy.yml
build-overview-pdf.yml
Create it in the order of §4 → §9. The baseline first is not arbitrary: the secret scanner
and .gitignore have to exist before the first file that could carry a credential does.
4. The repository baseline
This is REPO-BASELINE.md applied at t=0, and it is the
section most likely to be skipped as "not the interesting part". Every item in its §1 table
exists because something in the estate was lost to its absence.
Hygiene files (§1): CODEOWNERS, a real .editorconfig, a real .gitattributes —
not the stock template with every rule commented out — and an exclusion-based
.dockerignore, so a backend image build does not ship web/, tests/ and docs/ as
build context.
Dependency update automation: configured, and off..github/dependabot.yml is
written with every ecosystem the repo actually has (nuget, npm, github-actions, docker)
and a schedule for each, and every one of them carries
open-pull-requests-limit: 0 — Dependabot's own switch for "declared but opening no
version-update pull requests". Turning it on later is one line per ecosystem against a
file somebody already reviewed, rather than a research task on the day it is wanted.
This is a deliberate deviation from REPO-BASELINE.md
§1, which lists dependency-update automation as a baseline item, so it is recorded as
one: an ADR under §9 and a dated row in the deviation register, never a silent
omission. The reasoning the ADR should carry, because it is what makes the deviation
reviewable rather than merely convenient:
A repository on its first day has no one to own a bump. Automation that opens pull
requests nobody triages produces the thing baselines exist to prevent — a queue that
is ignored until it is closed wholesale, after which the repo has worse dependency
hygiene than one with no automation, because everyone believes it is covered.
The half of §1's concern that is actually about risk is already covered from the
first commit and not by Dependabot: NuGetAuditMode=all with
NuGetAuditLevel=low in Directory.Build.props fails the restore on a vulnerable
package, transitive ones included, and the CodeQL workflow (§8) runs the dependency
audit. What is deferred is version freshness, not vulnerability detection.
Dependabot security updates and vulnerability alerts are a repository setting, not
this file, so "off by default" here does not turn those off. Say so in the ADR, and
say which way the repository setting was left, or the next reader will assume the
wrong one.
Name the trigger for turning it on — the repo has a maintainer who triages, or it
goes public — so the row has an exit condition instead of becoming permanent by
forgetting.
Central package management (§1): Directory.Build.props for everything that is not a
version (TFM, Nullable, warnings-as-errors, package metadata) and
Directory.Packages.props for every version, exactly once. Projects reference packages by
name with no Version attribute. Pin the SDK band in global.json.
Secret hygiene (§2, P5): .gitleaks.toml, the pre-commit hook, and the CI job — both,
because the hook catches the mistake before it becomes history and the job catches
contributors without hooks. .env is gitignored; secrets.env.example is committed and
documents every variable with its tier (always / mode / secret / ci / tuning) and a
line saying what degrades without it (P8).
One-command onboarding (§3): scripts/setup.sh (and setup.ps1 — a generation
instruction that only works on one platform fails at step one of onboarding,
IDENTITY-AND-ACCOUNTS.md §10), structured as
numbered steps: prerequisites with install pointers, initialize dotnet user-secrets,
generate the one mandatory secret rather than asking for one, then each optional
integration as a labelled optional step. Pair it with a troubleshooting table keyed on
the literal exception text, and document the secret's journey once: local store →
AppHost parameter → environment variable → config key.
Scripts (§4): a scripts/README.md listing every variable by tier. The numbered
runbook convention matters when there is a runbook; at t=0 there is setup and the
scanner, and the convention is what the next script joins.
Standards adoption declared, not remembered (§7): commit .claude/settings.json.
Agent definitions in-repo (§6): under .github/agents/, with allowlisted tools as a
safety boundary, descriptions carrying worked invocation examples — the description is
the router — and repo-relative paths only.
5. The solution
The AppHost is the composition root, and it exists for development (P1). One command —
dotnet run --project src/<Pascal>.AppHost — brings the system up. Declare Postgres, the
API and the frontend with WithReference, WaitFor and WithHttpHealthCheck, and nothing
else: the AppHost is not the production topology, and its publish branch is a manifest
generator, not a second runtime. No secret is ever a literal here; parameters come from
dotnet user-secrets via builder.AddParameter(..., secret: true) (P5).
The kernel is a kernel (P2). <Pascal>.ServiceDefaults carries exactly the eight
concerns in P2's table — telemetry, health, discovery, resilience, JWT, CORS, OpenAPI,
database provider — as extension methods over IHostApplicationBuilder,
IServiceCollection or WebApplication. No base classes, no ModuleBase. A service opts
in line by line:
Install the ceiling now, not after the second violation. P2's limit is mechanical
because prose has already failed twice in this estate: a CI step that fails when the kernel
exceeds ~800 lines, and an architecture test asserting the kernel references no entity type.
Both cost ten minutes at t=0 and are the only thing standing between the kernel and a
Seeds.cs.
One service, and it owns its database (P3, P4). <Pascal>.Api:
Program.cs is a manifest (P9): each block one call into the service's own
ServiceCollectionExtensions, transport thin — bind, authorize, delegate.
Persistence is provider-portable: DATABASE_PROVIDER selects PostgreSQL or SqlServer and
falls back to InMemory with no connection string, so tests and a fresh clone need no
container.
Schema moves by MigrateAsync from provider-specific migrations, applied by a hosted
service after Kestrel starts — never EnsureCreated outside the InMemory path, and
never blocking the listener, so probes answer while schema work is in flight. Reference
data is seeded separately, never through HasData.
/health and /alive via MapDefaultEndpoints(), and /health reports the state of
every optional integration while the startup banner prints the same list (P8) — from the
first commit, because the list is what makes a degraded deployment diagnosable in one
request.
Endpoints follow SERVICE-API-PATTERNS.md: an
endpoint group at the right trust level, the validation filter, every list clamped, one
uniform error shape. Outbound HttpClients carry the standard resilience handler with
explicit timeouts.
Auth is validation only: RS256 tokens verified against authservice's JWKS. Exactly one
service in the estate holds a signing key, and it is not this one (P5).
Observability is part of the scaffold, not a follow-up (P15): OTLP traces, metrics and
logs, with health-check requests filtered out of traces.
One test project (P13), tests/<Pascal>.Api.Tests, mirroring the source tree, isolated
by constructor with a fresh InMemory database per test. Give it one real test — the
health endpoint's degraded-integration report is a good first one — rather than a
placeholder: a suite of Assert.True(true) is worse than an empty folder, because it
counts. tests/e2e gets one Playwright journey and its CI wiring in the same commit;
a suite no job runs is documentation that lies
(E2E-ACCEPTANCE-TESTING.md).
6. The product surface
Next.js, per FRONTEND-BFF.md — the estate's documented,
evidenced standard, not a default picked for this template. The browser talks only to its
own origin; everything else follows from that.
One pnpm workspace from the first commit, even with one app in it (§7). The workspace
is cheap now and is what makes the second app a move rather than a rewrite; the
@<slug>/web-kit package is created when that second app arrives, and the guide's
anti-example is four apps that each hand-copied their BFF routes.
Runtime configuration (§2): a dynamic GET /api/config route reading the environment
per request, with a short Cache-Control. No NEXT_PUBLIC_* for anything
environment-specific — those are baked at build time and cost you one image per
environment, which breaks build-once-deploy-many (P12).
Sessions (§3): tokens go from the auth callback to the app's own BFF route, which
sets them httpOnly, secure, sameSite: strict; a GET /api/auth/session route
rehydrates client state; logout deletes with the same attributes it set.
Middleware verifies, not decodes (§4): jwtVerify against authservice's JWKS with
issuer and audience, an explicit public-route list, ?redirect= preserved, and carve-outs
so OAuth callbacks keep their query strings. Middleware is UX; the API is the boundary.
One catch-all proxy (§5), /api/proxy/[...path]: prefix → backend routing, each
backend resolved through the candidate ladder (explicit env var → service-discovery
variables → internal DNS → localhost), bearer injected server-side from the cookie,
binaries streamed with a timeout sized for a scale-to-zero cold start (P7).
The Dockerfile is the three-stage deps → builder → runner pattern with standalone
output and a non-root nextjs user (P6).
7. Containers and the Fly.io topology
One container per service, multi-stage (P6): project files copied and restored before
source, runtime image major equal to the TFM major — roll-forward does not cross a major
and getting it wrong is a startup failure, not a warning — ASPNETCORE_URLS=http://+:8080,
EXPOSE 8080, LABEL org.opencontainers.image.source, non-root where the base image
allows, and any native dependency declared rather than assumed.
Four Fly apps, three shapes, all from FLY-IO-DEPLOYMENT.md
§4–§5. Write each fly.toml with both [build] dockerfileandcontext declared —
the omitted context is the field that makes a build work locally and break in CI:
App
Shape
min_machines_running
Why
<slug>-postgres
database
n/a
No [http_service], no public listener, ever. Reached at <app>.internal:5432 over 6PN. [[mounts]] with initial_size, PGDATA at a subdirectory of the mount, --ha=false
<slug>-authservice-<env>
HTTP service
1
Every validator fetches its JWKS in-request, and re-fetches when the cache expires — Fly §7's named case for pinning. Runs the published image; its source is never vendored or modified
<slug>-api-<env>
HTTP service
1
The frontend's server side calls it in-request. Pin the machine, or the caller's timeout must comfortably exceed the cold start — the second option is written down far more often than it is configured
<slug>-web-<env>
frontend
0
Entered only from a browser: a cold start there is a slow first page, not a failed call
Service-to-service HTTP uses the public URL, not .internal — the proxy starts a
stopped machine and .internal does not — and the issuer URL is forced public regardless,
because it is stamped into iss and every validator fetches JWKS from it. [env] holds
only what may be public; connection strings, signing keys and API keys are Fly secrets, set
from the pipeline with --stage.
Write flyio/SECRETS.md (what is a secret, where it lives, how to set it) and
flyio/INFRASTRUCTURE-ANALYSIS.md answering Fly §13's four questions — what runs when
nothing is happening, which services pin a machine and which synchronous call forces it,
what the cheaper option actually costs, and what is off the table. "Could be optimised" is
not an answer; a decision someone can take is.
8. CI/CD
Seven workflows, and every one of them wired on the first commit — a workflow added later
is a workflow that was never a gate.
Workflow
Trigger
What it does
ci.yml
PR + push to the default branch
Build, test, lint, the kernel size check and the architecture test from §5, the E2E suite
secret-scan.yml
PR + push
The scanner CI half (P5)
codeql.yml
PR + schedule
SAST plus the dependency audit
flyio.yml
tag v*
test → detect-changes → build once → ordered deploy
flyio-scale.yml
workflow_dispatch
Normalize machine counts; stateful apps excluded
flyio-destroy.yml
workflow_dispatch
Teardown behind a typed confirmation, reverse dependency order, volume kept by default
build-overview-pdf.yml
workflow_dispatch only
§9's PDF, as a run artifact
The deploy workflow is the one with rules that are easy to get subtly wrong (Fly §10):
Trigger on a tag, not a branch push. A deploy is an act; the tag is its record.
Change detection compares against the previous tag, not the previous commit, and a
change to the kernel or the contracts project invalidates every image that compiles them.
A service whose Fly app does not exist is always selected. This is the rule that lets
a cold estate come up from a single tag with no manual fly launch, and it is the one
most often missing — at t=0 nothing exists, so without it the first tag deploys nothing.
Build once, deploy many: one image per changed service, then flyctl deploy --image.
fail-fast: false on the matrix.
Order: postgres → authservice → api → web, and every gate accepts
success || skipped from upstream. Without the || skipped, change detection and
ordering fight each other and any unchanged service blocks everything behind it.
Gate the database separately — redeploying Postgres restarts it.
App and volume creation are idempotent and live in the workflow, never in someone's
shell history (Fly §11).
One post-deploy assertion the health check cannot make: that the JWKS endpoint
returns a non-empty key set. A missing key selects the symmetric path and publishes a
syntactically valid, empty JWKS — nothing reports unhealthy, the deploy goes green, and
every consumer rejects every token.
The one-time human setup is three lines and is written into flyio/SECRETS.md:
fly tokens create org stored as FLY_API_TOKEN in a GitHub environment, the root
secrets that environment needs, and nothing else.
9. Documentation
P14: documentation lives in the repository and records reasoning, not just steps. At t=0
that means four things, and a fifth if the system will ever hand somebody a PDF.
README.md written for a stranger: what the system is, how to run it in one command,
the project table, and the badge header row per
README-BADGES.md — every badge a titled link, and nothing
badged that does not exist. Do not describe a service the template did not create: a
stale README is a review finding on the day it is written, not months later.
AGENTS.md: how an agent should work in this repository, pointing at the standards
rather than restating them.
docs/architecture/00-ARCHITECTURE.md: this repository measured against the
constitution, referencing it rather than copying it, plus the deviation register
(§3a) — created empty, with the rule that every row carries a date and a reason. An
acknowledged deviation is a decision; an unacknowledged one is drift.
docs/adr/: 0000-template.md, 0001-record-architecture-decisions.md, and one ADR
for each decision §1, §4 and §7 actually took — the region, the registry, whether the
system has users, dependency automation declared but off, and anything you defaulted that
a reader might otherwise think was inevitable.
docs/ux/UI-UX.md: the screens and flows as scaffolded, and a ranked backlog, so the
first delivery session picks the backlog up instead of re-deriving it.
The PDF track, per 00-RESEARCH-DOCUMENTATION.md:
a project overview is not research, and the standard is explicit about the difference. If
you create one, it goes in docs/papers/ — not docs/research/papers/ — its .tex header
comment states plainly that it is not a research paper under that standard, it introduces no
fact its markdown source does not carry, and its workflow is workflow_dispatch-only,
because a document with no release cadence should not pretend to have one. Diagrams have one
source (docs/diagrams/<slug>.mmd, one per file) rendered for the PDF, never redrawn in
TikZ. Only the .tex is committed: .gitignore covers *.aux, *.log, *.out,
*.toc and the PDF itself, because a checked-in PDF disagrees with its source the first
time the source changes.
10. Verification
Nothing is committed until these have been run. A gate you could not run is reported as
not run — never implied to have passed, and never quietly dropped.
dotnet build clean, with warnings as errors.
dotnet test green, and the test it ran actually asserts something.
P8's literal test: dotnet run --project src/<Pascal>.AppHost with an empty .env
and zero cloud credentials produces a working system with reduced features. /health
answers 200 and lists which integrations are degraded; the startup banner says the same.
If it needs a credential to start, an integration was registered unconditionally.
Each image builds, and the container answers /health on :8080 — the port
internal_port claims.
The frontend builds to standalone output, and /api/config returns addresses read at
request time rather than baked in.
The secret scanner runs clean over the tree.
Workflow syntax checked (actionlint where available), and every relative link in the
README and the docs resolves.
scripts/setup.sh --check runs on a machine that has nothing installed and reports what
is missing by name, with install pointers.
Report the results as a short table. The three that people skip are 3, 4 and 8, and each
one is a promise the repository is now making to every future contributor.
11. Commit, push, and the first deploy
Commit in a small ordered series — baseline, solution, frontend, infrastructure, docs — so
the pull request is reviewable, and push to the branch from §2. Follow the repository's own
PR template if it has one; you created it in §4, so it does.
The first tag is what provisions the estate. With §8's "missing app ⇒ always selected"
rule in place, pushing v0.1.0 creates every Fly app and every volume from cold, in order,
with no fly launch and nothing configured by hand. Before tagging, confirm the GitHub
environment holds FLY_API_TOKEN and the root secrets; without them the deploy fails at
the first flyctl call, which is the correct place to fail.
The definition of done is the public URL. Not a green workflow: https://<slug>-web-<env>.fly.dev
serves the app, https://<slug>-api-<env>.fly.dev/health answers with its integration list,
and — if the system has users — one end-to-end journey works: register, log in, use the one
feature the template shipped. Report what is live, what degraded and why, and what the next
session should pick up from docs/ux/UI-UX.md.
If the deploy is out of scope for this run, say so explicitly and leave the repository at
"builds, tests green, images build, nothing deployed" rather than implying a live system.
12. Scope, defaults and stop conditions
What this template deliberately does not create, straight from the constitution's §4
non-goals — so that scope creep has something to fail against:
No second service. One bounded context, one database. A second service is a design
decision with a reason, not a scaffolding default (P3).
No event bus, no service mesh, no sidecars. Synchronous HTTP against a published
contract, plus per-service JWT, is the whole model at this size. Introducing a broker is
an explicit, recorded decision.
No custom DI container, no abstraction over the platform. fly.toml is written
directly.
No user store and no token minting anywhere in this repository (P5).
No sample domain model. The template ships the mechanism and one thin vertical slice;
inventing entities for a product you have not been told about produces code the first
ticket deletes.
Stop, say what the problem is, and ask — before creating files — when:
the repository is not empty (§2);
the name does not match ^[a-z][a-z0-9-]*$, or no name can be resolved (§1);
you are asked for a framework other than the ones the standards evidence — Next.js and
.NET Aspire are the documented standard, and a different choice is a recorded decision in
docs/adr/, not a silent substitution;
a standard and the request genuinely conflict. Record it as a deviation with its reason;
a deviation you keep is a decision, and one you leave silent is a finding somebody else
will file.
13. Failure modes
Symptom
Cause
The template lands on top of an application somebody already wrote
§2's emptiness check skipped; this procedure is for an empty repository, a drifted one needs MASTER-PROMPT.md
A fresh clone will not start without a cloud credential
An optional integration was registered unconditionally — P8's test in §10 catches this and was not run
The first tag deploys nothing at all
Change detection has no "missing Fly app ⇒ always selected" rule, so at t=0 every service looks unchanged (Fly §10)
fly deploy fails: app name already taken
app not namespaced with system and environment; the name is globally unique across all of Fly (Fly §4)
The build works locally and breaks in CI
[build] context not declared, so the context depended on where flyctl was invoked
initdb: directory not empty on the first Postgres boot
PGDATA points at the mount root, where lost+found lives; use a subdirectory (Fly §8)
The second database machine comes up empty
A volume app was deployed without --ha=false — a second machine gets a second empty volume, not a replica
Staging frontend calls the production API
Addresses baked via NEXT_PUBLIC_* at build time instead of the runtime /api/config route (FRONTEND-BFF §2)
A forged token is accepted at the edge
Middleware decoded the JWT instead of verifying its signature against JWKS
Deploy is green and every token is rejected
The signing key was missing, so the symmetric path was selected and an empty JWKS published; §8's post-deploy assertion exists for exactly this
The schema freezes at first-boot state
EnsureCreated on a real provider instead of MigrateAsync from migrations (P4)
The kernel carries domain by month two
The size check and the architecture test were left for later; prose has already failed twice in this estate (P2)
The README is already untrue at the first pull request
It was written from the plan rather than from the tree (P14)
A credential is committed in week one
The scanner and .gitignore were added after the code rather than before it (§4)
Twenty dependency pull requests on a repo with one commit, then all of them closed unread
Dependency automation was switched on before anyone owned the triage; §4 declares it and leaves it at open-pull-requests-limit: 0 for exactly this
A year in, every dependency is stale and nobody decided that
The §4 deviation was recorded without a trigger for reversing it, so "off for now" became "off" by forgetting
The E2E suite exists and has never run
It was committed without its CI wiring; wiring is part of a suite's definition of done
14. Checklist
Constitution and guides confirmed readable before the first file; stopped and said so if not
Repository name resolved from the argument (or the current repository) and echoed back; derived-names table echoed before anything was created
Region, registry and whether the system has users decided explicitly and recorded as ADRs
Repository confirmed empty; a non-empty one sent to the playbook or the master prompt instead
Toolchain checked and reported; every gate in §10 either run or reported as not run
Baseline first: hygiene files, central package management, .gitleaks.toml + hook + CI job, gitignored .env with a committed secrets.env.example carrying tiers and degrade lines
dependabot.yml declares every ecosystem at open-pull-requests-limit: 0; the deviation from REPO-BASELINE.md §1 carries an ADR, a dated register row, the state of the repository's security-update setting, and a trigger for turning it on
One-command setup on both platforms: prerequisites, secret store, the mandatory secret generated, optional integrations labelled, troubleshooting keyed on exception text
.claude/settings.json declares the marketplace and enables architecture-core
AppHost declares every resource with WithReference, WaitFor and WithHttpHealthCheck; no secret literal; publish branch treated as a manifest generator
Kernel holds only P2's eight plumbing concerns; the size check and the no-entity architecture test are in CI from the first commit
The service owns its database; provider-portable; MigrateAsync in a hosted service
…(truncated)
1---2name: init-generic-template3description: Use when starting a brand-new repository from nothing - the repo name is passed and the repo is empty. Scaffolds the estate's default containerized application in one pass: an Aspire AppHost composition root, a ServiceDefaults kernel with its size and no-domain guards, one service owning its database and migrating it from a hosted service, a Next.js product surface with runtime config, cookie sessions and the catch-all BFF proxy, multi-stage Dockerfiles, a Fly.io app per service, tag-driven workflows that provision a cold estate from the first tag, the repository baseline (secret scanning, one-command onboarding, central package management), and the docs set including ADRs and the LaTeX PDF track. Derives every name from the repo name and runs the verification gates before the first commit. Refuses a repo that already holds an application - that is a modernize or delivery session, not an init.4---56# Initializing a repository from the generic template78You have been given the name of a repository that is empty, and you are going to leave it9holding the estate's default containerized application: an Aspire composition root, a10shared kernel, one service that owns its database, a Next.js product surface, a container11per service, a Fly.io topology, tag-driven CI/CD, and the documentation set — all of it12compliant with [the constitution](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/architecture/00-REFERENCE-ARCHITECTURE.md) on the13first commit rather than on a later cleanup pass.1415One rule outranks everything below, because breaking it is the most expensive failure mode16in this estate: **read the standards, do not re-derive them.** Every rule here is stated in17shorthand — "the kernel stays a kernel", "one image, N environments" — and each one names18the principle or the guide section it compresses. Scaffolding is where re-derivation is19most tempting and most costly, because a wrong default gets copied into every file the20repository will ever have.2122**The argument is the repository name.** Everything else is derived from it (§1) or is a23default this document fixes, so that two repositories initialized a year apart come out the24same shape. What is genuinely a decision — the region, the registry, whether the system has25users — is asked once and recorded as an ADR, never guessed silently.2627This is the greenfield entry point. Its siblings: [`PLAYBOOK.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/PLAYBOOK.md) is for a28repository that already holds code (REVIEW / MODERNIZE / RECOVER),29[`MASTER-PROMPT.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/MASTER-PROMPT.md) is the delivery session that brings a drifted30application back to the standards and leaves it live, and31[`docs/delivery/WORKFLOW.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/delivery/WORKFLOW.md) is the per-ticket procedure that32takes over once this repository has its first ticket. **If the repository is not empty,33this procedure is the wrong one** (§2).3435**Contents**36370. [The standards have to be in front of you](#0-the-standards-have-to-be-in-front-of-you)381. [The argument, and the names derived from it](#1-the-argument)392. [Preflight: the repository has to be empty](#2-preflight)403. [The tree this creates](#3-the-tree-this-creates)414. [The repository baseline](#4-the-repository-baseline)425. [The solution: AppHost, kernel, contracts, one service](#5-the-solution)436. [The product surface: Next.js and the BFF](#6-the-product-surface)447. [Containers and the Fly.io topology](#7-containers-and-the-flyio-topology)458. [CI/CD](#8-cicd)469. [Documentation, including the PDF track](#9-documentation)4710. [Verification: the gates before the first commit](#10-verification)4811. [Commit, push, and the first deploy](#11-commit-push-and-the-first-deploy)4912. [Scope, defaults and stop conditions](#12-scope-defaults-and-stop-conditions)5013. [Failure modes](#13-failure-modes)5114. [Checklist](#14-checklist)5253---5455## 0. The standards have to be in front of you5657Confirm, before creating the first file, that you can actually open58[`00-REFERENCE-ARCHITECTURE.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/architecture/00-REFERENCE-ARCHITECTURE.md) and the59guides under [`docs/guides/`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/) — because60`architecture-core@architecture-standards` is installed, or because this repository is61attached to the session. **If you cannot, stop and say so.**6263The guides this procedure leans on directly, and which section owns which decision:6465| Domain | Guide | What it decides here |66|---|---|---|67| Hygiene, onboarding, scripts | [`REPO-BASELINE.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/REPO-BASELINE.md) | Everything in §4 |68| Deployment | [`FLY-IO-DEPLOYMENT.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/FLY-IO-DEPLOYMENT.md) | §7 and §8 |69| Frontend | [`FRONTEND-BFF.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/FRONTEND-BFF.md) | §6 |70| Service plumbing | [`SERVICE-API-PATTERNS.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/SERVICE-API-PATTERNS.md) | §5's endpoint and HTTP-client rules |71| Identity | [`IDENTITY-AND-ACCOUNTS.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/IDENTITY-AND-ACCOUNTS.md) | §5's auth wiring, §7's authservice app |72| Tests | [`TESTING-STRATEGY.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/TESTING-STRATEGY.md) | §5's test project, §10's gates |73| README and badges | [`README-BADGES.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/README-BADGES.md) | §9's header row |74| PDFs and diagrams | [`00-RESEARCH-DOCUMENTATION.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/research/00-RESEARCH-DOCUMENTATION.md) | §9's LaTeX track |7576Load the guide for a layer before you write that layer's files, and say which ones you77loaded. A scaffold is twenty small decisions taken at once; the guides are where nineteen78of them are already made.7980## 1. The argument8182`/init-generic-template <repo-name>` — the repository this run initializes, as its GitHub83name: lowercase, hyphen-separated, matching `^[a-z][a-z0-9-]*$`.8485**Without an argument, fall back to the repository you are already in** — its `origin`86remote, or the working directory's name — and echo that name back before touching87anything. If there is no such repository, stop and ask for the name. Do not invent one88from the conversation's subject: every name below is derived from this string, and a89plausible-but-wrong system name is baked into a Fly app that has to be destroyed to be90renamed.9192Everything else is derived, mechanically. Echo this table back as your first output, filled93in, so a wrong derivation is caught before it is written into forty files:9495| Derived | Rule | Example, from `zoo-companion` |96|---|---|---|97| System slug | the argument, unchanged | `zoo-companion` |98| .NET root namespace and solution | PascalCase of the slug, hyphens dropped | `ZooCompanion` |99| Projects | `<Pascal>.AppHost`, `.ServiceDefaults`, `.Contracts`, `.Api`, plus `.Api.Tests` | `ZooCompanion.AppHost` |100| Frontend workspace | `web/`, npm scope `@<slug>` | `@zoo-companion/web-kit` |101| Fly apps | `<slug>-<service>-<env>` — globally unique across all of Fly, not just your org ([`FLY-IO-DEPLOYMENT.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/FLY-IO-DEPLOYMENT.md) §4) | `zoo-companion-api-dev` |102| Fly Postgres app | `<slug>-postgres` — one instance, no environment suffix, databases inside it per service (P3, Fly §8) | `zoo-companion-postgres` |103| Databases | `<service>db` inside that instance | `apidb` |104| Container images | `ghcr.io/<owner>/<slug>-<service>` | `ghcr.io/konradcinkusz/zoo-companion-api` |105| Config prefix | the PascalCase namespace, for options sections | `ZooCompanion__…` |106107Three things are decisions rather than derivations, and each one is asked once and recorded108as an ADR under §9 — never defaulted silently:109110- **`primary_region`.** Pick the region nearest the users. The guides' examples show both111 `waw` and `fra`; neither is a default, and a region is expensive to change once volumes112 exist (Fly §8).113- **The registry.** The constitution's §2 disagreement table settles this as **GHCR** —114 portable and free at this scale — while the Fly guide's pipeline (§10) is written against115 `registry.fly.io`. Take GHCR, deploy with `flyctl deploy --image ghcr.io/…`, and note in116 the ADR that a private GHCR image needs a pull credential set as a Fly secret. Choosing117 `registry.fly.io` instead is allowed and is the same one-line ADR.118- **Whether the system has users.** If it does, identity is119 [`konradcinkusz/authservice`](https://github.com/konradcinkusz/authservice) adopted as an120 external service (§7); no user store and no token minting is ever written into this121 template (P5). If it does not, say so in the ADR and skip every auth step — a login page122 for a system with no accounts is scaffolding nobody asked for.123124## 2. Preflight125126**The repository has to be empty.** Empty means: nothing tracked beyond what a fresh GitHub127repository creates for itself — `.git/`, `README.md`, `LICENSE`, `.gitignore`. Anything128else — a solution, a `package.json`, a workflow, a `src/` — means this repository already129has a shape, and dropping a template on top of it silently overwrites decisions somebody130made.131132**If it is not empty, stop and say so**, and name the procedure that fits instead:133[`PLAYBOOK.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/PLAYBOOK.md) for a review or a modernization, or134[`MASTER-PROMPT.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/MASTER-PROMPT.md) for a delivery session that aligns a working135application and leaves it live. Do not "merge" the template into an existing tree.136137Then check the toolchain and report it, because §10's gates are only as good as what you138can actually run:139140| Tool | Needed for | If absent |141|---|---|---|142| .NET SDK | build, test, the AppHost | Blocking — the solution cannot be verified |143| Node + pnpm | the frontend build | Blocking for §6's gate only |144| A container engine | the image gates in §10 | Record the gate as not run; do not claim it passed |145| `flyctl` | nothing in this procedure | Not needed — §11 provisions from CI, never from a laptop (Fly §11) |146| `gitleaks` | §10's secret scan | Record it; the CI job in §8 still runs |147148Confirm the branch you are on and that it is not the default branch, and create one if149needed. Every file below lands in one branch and one pull request.150151## 3. The tree this creates152153One system, five projects, one frontend, three Fly apps. This is deliberately the *smallest*154system that exercises every principle — not a demonstration of every guide at once (§12).155156```157<repo>/158├─ .claude/settings.json declares the marketplace + architecture-core (REPO-BASELINE §7)159├─ .editorconfig · .gitattributes · .gitignore · .dockerignore160├─ .gitleaks.toml · secrets.env.example161├─ CODEOWNERS (in .github/) · LICENSE · README.md · AGENTS.md162├─ global.json pinned SDK band + Aspire SDK163├─ Directory.Build.props TFM, nullable, warnings-as-errors, metadata164├─ Directory.Packages.props central package management — one version per package165├─ <Pascal>.sln166├─ src/167│ ├─ <Pascal>.AppHost/ P1 — the composition root, development only168│ ├─ <Pascal>.ServiceDefaults/ P2 — the shared kernel: plumbing, nothing else169│ ├─ <Pascal>.Contracts/ DTOs that cross a service boundary170│ └─ <Pascal>.Api/ the one service: owns apidb, exposes /health + /alive171│ └─ Dockerfile P6 — multi-stage, :8080, non-root172├─ web/ one pnpm workspace (FRONTEND-BFF §7)173│ └─ app/ the Next.js product surface + Dockerfile174├─ tests/175│ ├─ <Pascal>.Api.Tests/ xUnit, InMemory provider — P13's logic-bearing layer176│ └─ e2e/ Playwright, wired to CI from the first commit177├─ flyio/178│ ├─ api.fly.toml · web.fly.toml · postgres.fly.toml · authservice.fly.toml179│ ├─ SECRETS.md what is secret, where it lives, how to set it180│ └─ INFRASTRUCTURE-ANALYSIS.md topology, sizing, cost — Fly §13's four questions181├─ scripts/182│ ├─ setup.sh · setup.ps1 one-command onboarding, numbered steps183│ ├─ hooks/pre-commit · scan-secrets.sh184│ └─ README.md every variable by tier185├─ docs/186│ ├─ architecture/00-ARCHITECTURE.md references the constitution; carries the deviation register187│ ├─ adr/0000-template.md + 0001… the decisions §1 and §7 took188│ ├─ ux/UI-UX.md screens, flows, ranked backlog189│ ├─ diagrams/*.mmd one diagram per file, one source (RESEARCH §"Diagrams in a PDF")190│ └─ papers/<slug>-overview.tex house style, explicitly not a research paper191└─ .github/192 ├─ CODEOWNERS · dependabot.yml (declared, limit 0) · pull_request_template.md · ISSUE_TEMPLATE/193 ├─ agents/ in-repo agent definitions, allowlisted tools194 └─ workflows/ ci.yml · secret-scan.yml · codeql.yml195 flyio.yml · flyio-scale.yml · flyio-destroy.yml196 build-overview-pdf.yml197```198199Create it in the order of §4 → §9. The baseline first is not arbitrary: the secret scanner200and `.gitignore` have to exist before the first file that could carry a credential does.201202## 4. The repository baseline203204This is [`REPO-BASELINE.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/REPO-BASELINE.md) applied at t=0, and it is the205section most likely to be skipped as "not the interesting part". Every item in its §1 table206exists because something in the estate was lost to its absence.207208- **Hygiene files** (§1): `CODEOWNERS`, a real `.editorconfig`, a real `.gitattributes` —209 not the stock template with every rule commented out — and an **exclusion-based210 `.dockerignore`**, so a backend image build does not ship `web/`, `tests/` and `docs/` as211 build context.212- **Dependency update automation: configured, and off.** `.github/dependabot.yml` is213 written with every ecosystem the repo actually has (nuget, npm, github-actions, docker)214 and a schedule for each, and **every one of them carries215 `open-pull-requests-limit: 0`** — Dependabot's own switch for "declared but opening no216 version-update pull requests". Turning it on later is one line per ecosystem against a217 file somebody already reviewed, rather than a research task on the day it is wanted.218219 This is a **deliberate deviation** from [`REPO-BASELINE.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/REPO-BASELINE.md)220 §1, which lists dependency-update automation as a baseline item, so it is recorded as221 one: an ADR under §9 and a dated row in the deviation register, never a silent222 omission. The reasoning the ADR should carry, because it is what makes the deviation223 reviewable rather than merely convenient:224225 - A repository on its first day has no one to own a bump. Automation that opens pull226 requests nobody triages produces the thing baselines exist to prevent — a queue that227 is ignored until it is closed wholesale, after which the repo has *worse* dependency228 hygiene than one with no automation, because everyone believes it is covered.229 - The half of §1's concern that is actually about *risk* is already covered from the230 first commit and not by Dependabot: `NuGetAuditMode=all` with231 `NuGetAuditLevel=low` in `Directory.Build.props` fails the restore on a vulnerable232 package, transitive ones included, and the CodeQL workflow (§8) runs the dependency233 audit. What is deferred is version *freshness*, not vulnerability detection.234 - **Dependabot security updates and vulnerability alerts are a repository setting, not235 this file**, so "off by default" here does not turn those off. Say so in the ADR, and236 say which way the repository setting was left, or the next reader will assume the237 wrong one.238 - Name the trigger for turning it on — the repo has a maintainer who triages, or it239 goes public — so the row has an exit condition instead of becoming permanent by240 forgetting.241- **Central package management** (§1): `Directory.Build.props` for everything that is not a242 version (TFM, `Nullable`, warnings-as-errors, package metadata) and243 `Directory.Packages.props` for every version, exactly once. Projects reference packages by244 name with no `Version` attribute. Pin the SDK band in `global.json`.245- **Secret hygiene** (§2, P5): `.gitleaks.toml`, the pre-commit hook, and the CI job — both,246 because the hook catches the mistake before it becomes history and the job catches247 contributors without hooks. `.env` is gitignored; `secrets.env.example` is committed and248 documents **every** variable with its tier (always / mode / secret / ci / tuning) and a249 line saying what degrades without it (P8).250- **One-command onboarding** (§3): `scripts/setup.sh` (and `setup.ps1` — a generation251 instruction that only works on one platform fails at step one of onboarding,252 [`IDENTITY-AND-ACCOUNTS.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/IDENTITY-AND-ACCOUNTS.md) §10), structured as253 numbered steps: prerequisites with install pointers, initialize `dotnet user-secrets`,254 **generate** the one mandatory secret rather than asking for one, then each optional255 integration as a labelled optional step. Pair it with a troubleshooting table keyed on256 the literal exception text, and document the secret's journey once: local store →257 AppHost parameter → environment variable → config key.258- **Scripts** (§4): a `scripts/README.md` listing every variable by tier. The numbered259 runbook convention matters when there is a runbook; at t=0 there is `setup` and the260 scanner, and the convention is what the next script joins.261- **Standards adoption declared, not remembered** (§7): commit `.claude/settings.json`.262263```json264{265 "extraKnownMarketplaces": {266 "architecture-standards": {267 "source": { "source": "github", "repo": "konradcinkusz/architecture-standards" }268 }269 },270 "enabledPlugins": {271 "architecture-core@architecture-standards": true272 }273}274```275276- **Agent definitions in-repo** (§6): under `.github/agents/`, with allowlisted tools as a277 safety boundary, descriptions carrying worked invocation examples — the description is278 the router — and **repo-relative paths only**.279280## 5. The solution281282**The AppHost is the composition root, and it exists for development** (P1). One command —283`dotnet run --project src/<Pascal>.AppHost` — brings the system up. Declare Postgres, the284API and the frontend with `WithReference`, `WaitFor` and `WithHttpHealthCheck`, and nothing285else: the AppHost is not the production topology, and its publish branch is a manifest286generator, not a second runtime. No secret is ever a literal here; parameters come from287`dotnet user-secrets` via `builder.AddParameter(..., secret: true)` (P5).288289**The kernel is a kernel** (P2). `<Pascal>.ServiceDefaults` carries exactly the eight290concerns in P2's table — telemetry, health, discovery, resilience, JWT, CORS, OpenAPI,291database provider — as extension methods over `IHostApplicationBuilder`,292`IServiceCollection` or `WebApplication`. No base classes, no `ModuleBase`. A service opts293in line by line:294295```csharp296builder.AddServiceDefaults();297builder.Services.AddJwtAuthentication(builder.Configuration);298builder.Services.AddCorsPolicy(builder.Configuration, CorsPolicies.Frontend);299builder.Services.AddDatabaseContext<ApiDbContext>(builder.Configuration, "apidb", "ApiInMemory");300```301302**Install the ceiling now, not after the second violation.** P2's limit is mechanical303because prose has already failed twice in this estate: a CI step that fails when the kernel304exceeds ~800 lines, and an architecture test asserting the kernel references no entity type.305Both cost ten minutes at t=0 and are the only thing standing between the kernel and a306`Seeds.cs`.307308**One service, and it owns its database** (P3, P4). `<Pascal>.Api`:309310- `Program.cs` is a manifest (P9): each block one call into the service's own311 `ServiceCollectionExtensions`, transport thin — bind, authorize, delegate.312- Persistence is provider-portable: `DATABASE_PROVIDER` selects PostgreSQL or SqlServer and313 falls back to InMemory with no connection string, so tests and a fresh clone need no314 container.315- **Schema moves by `MigrateAsync` from provider-specific migrations, applied by a hosted316 service after Kestrel starts** — never `EnsureCreated` outside the InMemory path, and317 never blocking the listener, so probes answer while schema work is in flight. Reference318 data is seeded separately, never through `HasData`.319- `/health` and `/alive` via `MapDefaultEndpoints()`, and `/health` reports the state of320 every optional integration while the startup banner prints the same list (P8) — from the321 first commit, because the list is what makes a degraded deployment diagnosable in one322 request.323- Endpoints follow [`SERVICE-API-PATTERNS.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/SERVICE-API-PATTERNS.md): an324 endpoint group at the right trust level, the validation filter, every list clamped, one325 uniform error shape. Outbound `HttpClient`s carry the standard resilience handler with326 explicit timeouts.327- Auth is validation only: RS256 tokens verified against authservice's JWKS. **Exactly one328 service in the estate holds a signing key, and it is not this one** (P5).329- Observability is part of the scaffold, not a follow-up (P15): OTLP traces, metrics and330 logs, with health-check requests filtered out of traces.331332**One test project** (P13), `tests/<Pascal>.Api.Tests`, mirroring the source tree, isolated333by constructor with a fresh InMemory database per test. Give it one real test — the334health endpoint's degraded-integration report is a good first one — rather than a335placeholder: a suite of `Assert.True(true)` is worse than an empty folder, because it336counts. `tests/e2e` gets one Playwright journey **and its CI wiring in the same commit**;337a suite no job runs is documentation that lies338([`E2E-ACCEPTANCE-TESTING.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/E2E-ACCEPTANCE-TESTING.md)).339340## 6. The product surface341342Next.js, per [`FRONTEND-BFF.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/FRONTEND-BFF.md) — the estate's documented,343evidenced standard, not a default picked for this template. The browser talks only to its344own origin; everything else follows from that.345346- **One pnpm workspace from the first commit**, even with one app in it (§7). The workspace347 is cheap now and is what makes the second app a move rather than a rewrite; the348 `@<slug>/web-kit` package is created when that second app arrives, and the guide's349 anti-example is four apps that each hand-copied their BFF routes.350- **Runtime configuration** (§2): a dynamic `GET /api/config` route reading the environment351 per request, with a short `Cache-Control`. No `NEXT_PUBLIC_*` for anything352 environment-specific — those are baked at build time and cost you one image per353 environment, which breaks build-once-deploy-many (P12).354- **Sessions** (§3): tokens go from the auth callback to the app's *own* BFF route, which355 sets them `httpOnly`, `secure`, `sameSite: strict`; a `GET /api/auth/session` route356 rehydrates client state; logout deletes with the same attributes it set.357- **Middleware verifies, not decodes** (§4): `jwtVerify` against authservice's JWKS with358 issuer and audience, an explicit public-route list, `?redirect=` preserved, and carve-outs359 so OAuth callbacks keep their query strings. Middleware is UX; the API is the boundary.360- **One catch-all proxy** (§5), `/api/proxy/[...path]`: prefix → backend routing, each361 backend resolved through the candidate ladder (explicit env var → service-discovery362 variables → internal DNS → localhost), bearer injected server-side from the cookie,363 binaries streamed with a timeout sized for a scale-to-zero cold start (P7).364- The Dockerfile is the three-stage `deps → builder → runner` pattern with `standalone`365 output and a non-root `nextjs` user (P6).366367## 7. Containers and the Fly.io topology368369**One container per service, multi-stage** (P6): project files copied and restored before370source, runtime image major equal to the TFM major — roll-forward does not cross a major371and getting it wrong is a startup failure, not a warning — `ASPNETCORE_URLS=http://+:8080`,372`EXPOSE 8080`, `LABEL org.opencontainers.image.source`, non-root where the base image373allows, and any native dependency declared rather than assumed.374375Four Fly apps, three shapes, all from [`FLY-IO-DEPLOYMENT.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/FLY-IO-DEPLOYMENT.md)376§4–§5. Write each `fly.toml` with both `[build] dockerfile` **and** `context` declared —377the omitted `context` is the field that makes a build work locally and break in CI:378379| App | Shape | `min_machines_running` | Why |380|---|---|---|---|381| `<slug>-postgres` | database | n/a | No `[http_service]`, no public listener, ever. Reached at `<app>.internal:5432` over 6PN. `[[mounts]]` with `initial_size`, `PGDATA` at a **subdirectory** of the mount, `--ha=false` |382| `<slug>-authservice-<env>` | HTTP service | **1** | Every validator fetches its JWKS in-request, and re-fetches when the cache expires — Fly §7's named case for pinning. Runs the published image; its source is never vendored or modified |383| `<slug>-api-<env>` | HTTP service | **1** | The frontend's server side calls it in-request. Pin the machine, or the caller's timeout must comfortably exceed the cold start — the second option is written down far more often than it is configured |384| `<slug>-web-<env>` | frontend | 0 | Entered only from a browser: a cold start there is a slow first page, not a failed call |385386Service-to-service HTTP uses the **public URL**, not `.internal` — the proxy starts a387stopped machine and `.internal` does not — and the issuer URL is forced public regardless,388because it is stamped into `iss` and every validator fetches JWKS from it. `[env]` holds389only what may be public; connection strings, signing keys and API keys are Fly secrets, set390from the pipeline with `--stage`.391392Write `flyio/SECRETS.md` (what is a secret, where it lives, how to set it) and393`flyio/INFRASTRUCTURE-ANALYSIS.md` answering Fly §13's four questions — what runs when394nothing is happening, which services pin a machine and which synchronous call forces it,395what the cheaper option actually costs, and what is off the table. "Could be optimised" is396not an answer; a decision someone can take is.397398## 8. CI/CD399400Seven workflows, and every one of them wired on the first commit — a workflow added later401is a workflow that was never a gate.402403| Workflow | Trigger | What it does |404|---|---|---|405| `ci.yml` | PR + push to the default branch | Build, test, lint, the kernel size check and the architecture test from §5, the E2E suite |406| `secret-scan.yml` | PR + push | The scanner CI half (P5) |407| `codeql.yml` | PR + schedule | SAST plus the dependency audit |408| `flyio.yml` | tag `v*` | test → detect-changes → build once → ordered deploy |409| `flyio-scale.yml` | `workflow_dispatch` | Normalize machine counts; stateful apps excluded |410| `flyio-destroy.yml` | `workflow_dispatch` | Teardown behind a typed confirmation, reverse dependency order, volume kept by default |411| `build-overview-pdf.yml` | `workflow_dispatch` only | §9's PDF, as a run artifact |412413The deploy workflow is the one with rules that are easy to get subtly wrong (Fly §10):414415- **Trigger on a tag, not a branch push.** A deploy is an act; the tag is its record.416- **Change detection compares against the previous tag**, not the previous commit, and a417 change to the kernel or the contracts project invalidates every image that compiles them.418- **A service whose Fly app does not exist is always selected.** This is the rule that lets419 a cold estate come up from a single tag with no manual `fly launch`, and it is the one420 most often missing — at t=0 *nothing* exists, so without it the first tag deploys nothing.421- **Build once, deploy many**: one image per changed service, then `flyctl deploy --image`.422 `fail-fast: false` on the matrix.423- **Order: postgres → authservice → api → web**, and every gate accepts424 `success || skipped` from upstream. Without the `|| skipped`, change detection and425 ordering fight each other and any unchanged service blocks everything behind it.426- **Gate the database separately** — redeploying Postgres restarts it.427- **App and volume creation are idempotent and live in the workflow**, never in someone's428 shell history (Fly §11).429- **One post-deploy assertion the health check cannot make**: that the JWKS endpoint430 returns a **non-empty** key set. A missing key selects the symmetric path and publishes a431 syntactically valid, empty JWKS — nothing reports unhealthy, the deploy goes green, and432 every consumer rejects every token.433434The one-time human setup is three lines and is written into `flyio/SECRETS.md`:435`fly tokens create org` stored as `FLY_API_TOKEN` in a GitHub **environment**, the root436secrets that environment needs, and nothing else.437438## 9. Documentation439440P14: documentation lives in the repository and records reasoning, not just steps. At t=0441that means four things, and a fifth if the system will ever hand somebody a PDF.442443- **`README.md` written for a stranger**: what the system is, how to run it in one command,444 the project table, and the badge header row per445 [`README-BADGES.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/README-BADGES.md) — every badge a titled link, and nothing446 badged that does not exist. Do not describe a service the template did not create: a447 stale README is a review finding on the day it is written, not months later.448- **`AGENTS.md`**: how an agent should work in this repository, pointing at the standards449 rather than restating them.450- **`docs/architecture/00-ARCHITECTURE.md`**: this repository measured against the451 constitution, **referencing** it rather than copying it, plus the deviation register452 (§3a) — created empty, with the rule that every row carries a date and a reason. An453 acknowledged deviation is a decision; an unacknowledged one is drift.454- **`docs/adr/`**: `0000-template.md`, `0001-record-architecture-decisions.md`, and one ADR455 for each decision §1, §4 and §7 actually took — the region, the registry, whether the456 system has users, dependency automation declared but off, and anything you defaulted that457 a reader might otherwise think was inevitable.458- **`docs/ux/UI-UX.md`**: the screens and flows as scaffolded, and a ranked backlog, so the459 first delivery session picks the backlog up instead of re-deriving it.460461**The PDF track**, per [`00-RESEARCH-DOCUMENTATION.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/research/00-RESEARCH-DOCUMENTATION.md):462a project overview is *not* research, and the standard is explicit about the difference. If463you create one, it goes in `docs/papers/` — not `docs/research/papers/` — its `.tex` header464comment states plainly that it is not a research paper under that standard, it introduces no465fact its markdown source does not carry, and its workflow is `workflow_dispatch`-only,466because a document with no release cadence should not pretend to have one. Diagrams have one467source (`docs/diagrams/<slug>.mmd`, one per file) rendered for the PDF, never redrawn in468TikZ. **Only the `.tex` is committed**: `.gitignore` covers `*.aux`, `*.log`, `*.out`,469`*.toc` and the PDF itself, because a checked-in PDF disagrees with its source the first470time the source changes.471472## 10. Verification473474Nothing is committed until these have been run. **A gate you could not run is reported as475not run** — never implied to have passed, and never quietly dropped.4764771. `dotnet build` clean, with warnings as errors.4782. `dotnet test` green, and the test it ran actually asserts something.4793. **P8's literal test**: `dotnet run --project src/<Pascal>.AppHost` with an empty `.env`480 and zero cloud credentials produces a working system with reduced features. `/health`481 answers 200 and lists which integrations are degraded; the startup banner says the same.482 If it needs a credential to start, an integration was registered unconditionally.4834. Each image builds, and the container answers `/health` on `:8080` — the port484 `internal_port` claims.4855. The frontend builds to `standalone` output, and `/api/config` returns addresses read at486 request time rather than baked in.4876. The secret scanner runs clean over the tree.4887. Workflow syntax checked (`actionlint` where available), and every relative link in the489 README and the docs resolves.4908. `scripts/setup.sh --check` runs on a machine that has nothing installed and reports what491 is missing by name, with install pointers.492493Report the results as a short table. The three that people skip are 3, 4 and 8, and each494one is a promise the repository is now making to every future contributor.495496## 11. Commit, push, and the first deploy497498Commit in a small ordered series — baseline, solution, frontend, infrastructure, docs — so499the pull request is reviewable, and push to the branch from §2. Follow the repository's own500PR template if it has one; you created it in §4, so it does.501502**The first tag is what provisions the estate.** With §8's "missing app ⇒ always selected"503rule in place, pushing `v0.1.0` creates every Fly app and every volume from cold, in order,504with no `fly launch` and nothing configured by hand. Before tagging, confirm the GitHub505environment holds `FLY_API_TOKEN` and the root secrets; without them the deploy fails at506the first `flyctl` call, which is the correct place to fail.507508**The definition of done is the public URL.** Not a green workflow: `https://<slug>-web-<env>.fly.dev`509serves the app, `https://<slug>-api-<env>.fly.dev/health` answers with its integration list,510and — if the system has users — one end-to-end journey works: register, log in, use the one511feature the template shipped. Report what is live, what degraded and why, and what the next512session should pick up from `docs/ux/UI-UX.md`.513514If the deploy is out of scope for this run, say so explicitly and leave the repository at515"builds, tests green, images build, nothing deployed" rather than implying a live system.516517## 12. Scope, defaults and stop conditions518519What this template deliberately does **not** create, straight from the constitution's §4520non-goals — so that scope creep has something to fail against:521522- **No second service.** One bounded context, one database. A second service is a design523 decision with a reason, not a scaffolding default (P3).524- **No event bus, no service mesh, no sidecars.** Synchronous HTTP against a published525 contract, plus per-service JWT, is the whole model at this size. Introducing a broker is526 an explicit, recorded decision.527- **No custom DI container**, no abstraction over the platform. `fly.toml` is written528 directly.529- **No user store and no token minting** anywhere in this repository (P5).530- **No sample domain model.** The template ships the mechanism and one thin vertical slice;531 inventing entities for a product you have not been told about produces code the first532 ticket deletes.533534**Stop, say what the problem is, and ask — before creating files — when:**535536- the repository is not empty (§2);537- the name does not match `^[a-z][a-z0-9-]*$`, or no name can be resolved (§1);538- you are asked for a framework other than the ones the standards evidence — Next.js and539 .NET Aspire are the documented standard, and a different choice is a recorded decision in540 `docs/adr/`, not a silent substitution;541- a standard and the request genuinely conflict. Record it as a deviation with its reason;542 a deviation you keep is a decision, and one you leave silent is a finding somebody else543 will file.544545## 13. Failure modes546547| Symptom | Cause |548|---|---|549| The template lands on top of an application somebody already wrote | §2's emptiness check skipped; this procedure is for an empty repository, a drifted one needs [`MASTER-PROMPT.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/MASTER-PROMPT.md) |550| A fresh clone will not start without a cloud credential | An optional integration was registered unconditionally — P8's test in §10 catches this and was not run |551| The first tag deploys nothing at all | Change detection has no "missing Fly app ⇒ always selected" rule, so at t=0 every service looks unchanged (Fly §10) |552| `fly deploy` fails: app name already taken | `app` not namespaced with system and environment; the name is globally unique across all of Fly (Fly §4) |553| The build works locally and breaks in CI | `[build] context` not declared, so the context depended on where `flyctl` was invoked |554| `initdb: directory not empty` on the first Postgres boot | `PGDATA` points at the mount root, where `lost+found` lives; use a subdirectory (Fly §8) |555| The second database machine comes up empty | A volume app was deployed without `--ha=false` — a second machine gets a second empty volume, not a replica |556| Staging frontend calls the production API | Addresses baked via `NEXT_PUBLIC_*` at build time instead of the runtime `/api/config` route (FRONTEND-BFF §2) |557| A forged token is accepted at the edge | Middleware decoded the JWT instead of verifying its signature against JWKS |558| Deploy is green and every token is rejected | The signing key was missing, so the symmetric path was selected and an empty JWKS published; §8's post-deploy assertion exists for exactly this |559| The schema freezes at first-boot state | `EnsureCreated` on a real provider instead of `MigrateAsync` from migrations (P4) |560| The kernel carries domain by month two | The size check and the architecture test were left for later; prose has already failed twice in this estate (P2) |561| The README is already untrue at the first pull request | It was written from the plan rather than from the tree (P14) |562| A credential is committed in week one | The scanner and `.gitignore` were added after the code rather than before it (§4) |563| Twenty dependency pull requests on a repo with one commit, then all of them closed unread | Dependency automation was switched on before anyone owned the triage; §4 declares it and leaves it at `open-pull-requests-limit: 0` for exactly this |564| A year in, every dependency is stale and nobody decided that | The §4 deviation was recorded without a trigger for reversing it, so "off for now" became "off" by forgetting |565| The E2E suite exists and has never run | It was committed without its CI wiring; wiring is part of a suite's definition of done |566567## 14. Checklist568569- [ ] Constitution and guides confirmed readable before the first file; stopped and said so if not570- [ ] Repository name resolved from the argument (or the current repository) and echoed back; derived-names table echoed before anything was created571- [ ] Region, registry and whether the system has users decided explicitly and recorded as ADRs572- [ ] Repository confirmed empty; a non-empty one sent to the playbook or the master prompt instead573- [ ] Toolchain checked and reported; every gate in §10 either run or reported as not run574- [ ] Baseline first: hygiene files, central package management, `.gitleaks.toml` + hook + CI job, gitignored `.env` with a committed `secrets.env.example` carrying tiers and degrade lines575- [ ] `dependabot.yml` declares every ecosystem at `open-pull-requests-limit: 0`; the deviation from `REPO-BASELINE.md` §1 carries an ADR, a dated register row, the state of the repository's security-update setting, and a trigger for turning it on576- [ ] One-command setup on both platforms: prerequisites, secret store, the mandatory secret generated, optional integrations labelled, troubleshooting keyed on exception text577- [ ] `.claude/settings.json` declares the marketplace and enables `architecture-core`578- [ ] AppHost declares every resource with `WithReference`, `WaitFor` and `WithHttpHealthCheck`; no secret literal; publish branch treated as a manifest generator579- [ ] Kernel holds only P2's eight plumbing concerns; the size check and the no-entity architecture test are in CI from the first commit580- [ ] The service owns its database; provider-portable; `MigrateAsync` in a hosted service 581582…(truncated)
Run npx skillmds@latest add konradcinkusz/init-generic-template in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Use when starting a brand-new repository from nothing - the repo name is passed and the repo is empty. Scaffolds the estate's default containerized application in one pass: an Aspire AppHost composition root, a ServiceDefaults kernel with its size and no-domain guards, one service owning its database and migrating it from a hosted service, a Next.js product surface with runtime config, cookie sessions and the catch-all BFF proxy, multi-stage Dockerfiles, a Fly.io app per service, tag-driven workflows that provision a cold estate from the first tag, the repository baseline (secret scanning, one-command onboarding, central package management), and the docs set including ADRs and the LaTeX PDF track. Derives every name from the repo name and runs the verification gates before the first commit. Refuses a repo that already holds an application - that is a modernize or delivery session, not an init. It is listed under Web & Frontend on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
konradcinkusz (@konradcinkusz) published this skill. Their other Agent Skills are listed on their SkillMD profile.