Bootstraps a new project repo from an empty state: asks the user for a project name, pre-flights the agent's environment, agrees on a folder layout, scaffolds the Aspire C# AppHost (project-based via dotnet new, with .csproj and .slnx solution), scaffolds the .NET gRPC services under `services/`, scaffolds the Angular SSR `admin` client under `clients/` with Tailwind CSS v4 for styling, wires buf + Connect proto-to-TypeScript codegen, adds an Envoy front proxy with static config, runs a pass/fail smoke check that proves end-to-end gRPC-Web connectivity from the Angular client through Envoy to the backend services, and documents a clean stop. Use when the user asks to initialize, bootstrap, scaffold, or set up a project, or when the repo only contains README/scripts/.cursor and no app code yet.
Resources
1Install
npx skillscat add duncan-g/protofast/bootstrap-project Install via the SkillsCat registry.
Bootstrap Project
End-to-end project initialization workflow. Run from the repo root
(the skill makes no assumption about absolute paths). Starting state:
only README.md, .gitignore, .editorconfig, .gitattributes,scripts/, and .cursor/.
Project name
Before doing anything else, ask the user for the project name.
This name is used as the root namespace for .NET projects (e.g.«ProjectName».Auth), in Envoy virtual-host labels, and in the
README. It should be PascalCase and short (e.g. ProtoJet,SkyBridge, Nimbus).
Throughout the rest of this skill, «ProjectName» is a placeholder.
Substitute the user's chosen name everywhere it appears — in shell
commands, file contents, folder-layout descriptions, and reference
files.
When followed end to end, this workflow produces a working stack: the
Aspire dashboard shows every resource Healthy, the Envoy proxy routes
all traffic, and the Angular admin client can call the gRPC Greeter
service end-to-end via gRPC-Web. There is no "MVP path with
known-broken routes" — the prescription is the working configuration.
If a step produces an unexpected symptom, consultreferences/cleanup-and-troubleshooting.md which maps symptoms to
causes.
Operating principles
Follow every rule below. Each is a load-bearing assumption for later
steps; deviations should be deliberate and discussed with the user.
Generator output is the contract. After every generator
(dotnet new,ng new, …), list the produced files and adapt.
The skill prescribes one expected shape per generator; if the
output differs, stop and ask before continuing.Specify intent, drop unknown flags. If a generator rejects a
flag withUnknown argument: ..., drop the flag and re-run. Do
not pin to a deprecated generator version just to keep the flag.The agent's shell is not the user's shell. Version managers
(nvm, fnm, n, volta, asdf, pyenv, sdkman, rbenv, …) load via
interactive-shell init scripts. The agent runs non-interactively
and must source them explicitly in every command that needs
them — including the command that launches Aspire, since
orchestrator child processes inherit the env at launch time.Aspire assigns all ports (except its own dashboard). No
service or client dev-server port is hardcoded. Aspire selects
every port at startup and passes it through endpoint references.
Envoy gets one HTTPS listener per client (fixed internal target
ports 20000, 20001, … assigned in registration order byWithClient); each listener routes API prefixes to the services
and its catch-all to that client. All upstream endpoints (hosts
and ports) are injected as environment variables viaWithUpstreamEndpointand rendered byentrypoint.shat
container startup (seereferences/envoy-proxy.md6c, 6h, 6j).
Do not add fixed port numbers tolaunchSettings.json; rely on
Aspire's dynamic assignment throughout.
4a. HTTPS and HTTP/3 are the default transport. Envoy terminates
TLS on a TCP listener (HTTP/2 + HTTP/1.1 via ALPN) and a UDP
listener (HTTP/3 via QUIC) on the same port. The Angular dev
server also serves HTTPS using Aspire's developer certificate.
Both use WithHttpsEndpoint and Aspire's
WithHttpsCertificateConfiguration to inject cert/key paths as
environment variables. Upstream clusters still use cleartext h2c
or HTTP/1.1 — TLS terminates at the edge.
«ProjectName»is a placeholder, not a literal. Every
occurrence of«ProjectName»in commands, file contents, and
comments must be replaced with the project name the user chose
at the start. The corresponding lowercase form (e.g.«projectname») is used for Envoy virtual-host names and similar
identifiers that conventionally use lowercase.Container ↔ host networking requires explicit handling. Every
container resource that reaches a host process follows Appendix A
(host-gateway hostname) and Appendix B (bind backends on0.0.0.0). The Steps below already apply these — don't strip the
calls.Every smoke-check expectation has a registration step. When
Step 9 lists N resources, Steps 2–7 contain N corresponding.Add...calls. Cross-reference by name.Pre-flight and cleanup are workflow steps, not optional. Step 0
and Step 10 are first-class. The orchestrator's stop command does
not always clean up the containers it spawned; pre-flight surfaces
any holdovers before they collide.Smoke check is pass/fail. Step 9 checks return pass or fail.
On the first failure, stop and report — don't silently work around.
There is no "documented caveat" middle category in this skill.
See Skill maintenance at the bottom for the rules the skill
author follows when updating this file.
Available MCP servers
The repo's .cursor/mcp.json exposes:
aspire—aspire agent mcp.angular-cli—npx -y @angular/cli mcp.aws-mcp— not needed for bootstrap.
Prefer MCP tools when the server's descriptors are populated for this
workspace; otherwise use the underlying CLI. Both achieve the same
effect. Either way, list a server's tools before calling them — don't
guess argument shapes.
Workflow checklist
Bootstrap Progress:
- [ ] Step 0: Pre-flight
- [ ] Step 1: Confirm folder layout with the user
- [ ] Step 2: Scaffold Aspire AppHost
- [ ] Step 3: Prepare shared libraries (rename namespaces, update packages)
- [ ] Step 4: Add .NET gRPC services (via add-dotnet-service)
- [ ] Step 5–6: Add Angular `admin` client + proto codegen (via add-angular-client)
- [ ] Step 7: Add Envoy proxy
- [ ] Step 8: Update README
- [ ] Step 9: Smoke check
- [ ] Step 10: Cleanup or graceful stopStep 0 — Pre-flight
Load references/preflight.md and run every check (0a–0g) before
any generator. Confirms: repo structure, Node + npm via version manager, Aspire CLI,
container runtime, no orphan AppHosts/containers, ports free, env
prepared for Aspire child processes.
Step 1 — Folder layout
Propose this layout and get explicit user confirmation before any
generator. If the user pushes back, adjust first, then rerun the
propose-confirm loop.
<repo root>/
├── «ProjectName».slnx # Solution file (all .csproj projects)
├── apphost/ # Aspire AppHost (project-based)
│ ├── «ProjectName».AppHost.csproj
│ ├── Program.cs
│ ├── aspire.config.json
│ ├── ClientApp/ # Reusable AddClientApp extension method
│ │ └── ClientAppResourceBuilderExtensions.cs
│ ├── EnvoyProxy/ # Envoy resource builder extensions
│ │ └── EnvoyProxyResourceBuilderExtensions.cs
│ └── Properties/
│ └── launchSettings.json
├── services/ # Backend .NET gRPC services
│ ├── shared/ # Shared libraries (pre-existing in template)
│ │ ├── ServiceDefaults/ # OpenTelemetry, health checks, service discovery
│ │ ├── Database/ # EF Core base classes + unit of work
│ │ ├── Database.Abstractions/ # Interfaces for DB layer
│ │ └── Exceptions/ # Common gRPC error descriptors
│ ├── auth/ # Auth service
│ │ ├── src/«ProjectName».Auth.Api/ # gRPC API project
│ │ │ └── Protos/ # .proto files (co-located with API project)
│ │ └── tests/ # Test projects (empty initially)
│ ├── payments/ # Payments service
│ │ ├── src/«ProjectName».Payments.Api/
│ │ │ └── Protos/
│ │ └── tests/
│ └── api/ # Api service
│ ├── src/«ProjectName».Api/
│ │ └── Protos/
│ └── tests/
├── clients/ # Angular SSR apps, one per client
│ ├── admin/ # Admin client (scaffolded during bootstrap)
│ │ ├── .postcssrc.json # PostCSS config (loads Tailwind v4 plugin)
│ │ ├── buf.gen.yaml # Proto codegen config (selects which protos)
│ │ └── src/lib/gen/ # Generated TS from protos (gitignored)
│ ├── host/ # Unified SSR host (serves all clients in publish mode)
│ │ ├── server.mjs # Express dispatcher (x-client header → client bundle)
│ │ ├── package.json
│ │ └── Dockerfile # Builds every client; repo root is the build context
│ └── app/ # End-user client (placeholder; not scaffolded)
├── proxy/ # Envoy config (Dockerfile + fragment templates)
│ ├── Dockerfile
│ ├── envoy.yaml.tmpl # Base config (admin, clusters, markers)
│ ├── envoy.listener.yaml.tmpl # One TCP+QUIC listener pair (per client in dev)
│ ├── envoy.rds.yaml.tmpl # Route-config shell
│ ├── envoy.vhost.yaml.tmpl # One virtual host (CORS, routes, catch-all)
│ ├── envoy.cluster.yaml.tmpl # One web upstream cluster
│ └── entrypoint.sh # Renders fragments per ENVOY_MODE, launches Envoy
├── scripts/ # Existing dev-setup scripts
├── .cursor/ # MCP + skills (existing)
├── .gitignore
└── README.mdRules:
- All generators write into the dirs above, never into the repo
root. Pass each generator's output flag. clients/app/stays a placeholder (clients/app/.gitkeeponly if
git needs the directory tracked).- Each service has
src/andtests/subdirectories. The gRPC API
project lives atservices/«name»/src/«ProjectName».«Name».Api/
(the.Apisuffix distinguishes it from Domain, Infrastructure,
etc. projects added later). Exception: when«Name»is alreadyApi, the project is just«ProjectName».Api(no double suffix).
Thetests/directory starts with a.gitkeepand is populated
when test projects are added. apphost/is lowercase as the folder name; the.csprojinside is
root-namespaced as«ProjectName».AppHost.- Each client has its own
buf.gen.yamlthat selects which service
protos to generate TypeScript clients for. Proto files live inside
each API project (e.g.services/auth/src/«ProjectName».Auth.Api/Protos/), not at the
service root.
Step 2 — Scaffold the Aspire AppHost and solution
The Aspire CLI's aspire new no longer supports a --project-based
flag and defaults to file-based AppHosts (#:sdk directive, no.csproj). Skip aspire new entirely — scaffold via dotnet new
and add the Aspire SDK manually.
2a. Create the console project:
dotnet new console -n «ProjectName».AppHost -o apphost --use-program-main false2b. Detect the Aspire SDK version and target framework:
dotnet --version | cut -d. -f1,2 # e.g. "10.0" → net10.0
dotnet package search Aspire.AppHost.Sdk --exact-match --take 1 --format json2c. Add the Aspire AppHost SDK toapphost/«ProjectName».AppHost.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="«AspireSdkVersion»" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net«MajorMinor»</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>2d. Write the initial apphost/Program.cs:
var builder = DistributedApplication.CreateBuilder(args);
builder.Build().Run();2e. Create apphost/aspire.config.json pointing at the .csproj:
{
"appHost": {
"path": "«ProjectName».AppHost.csproj"
}
}2f. Create apphost/Properties/launchSettings.json. dotnet run
(which aspire run invokes) only reads launchSettings.json for
project-based AppHosts — it does not use aspire.config.json
profiles. Without it the AppHost crashes with ASPNETCORE_URLS environment variable was not set. Use a standard profile:
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://0.0.0.0:21147",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22001",
"ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS": "true"
}
}
}
}2g. Create the solution file at the repo root and add the
AppHost plus all shared library projects:
dotnet new sln -n «ProjectName»
dotnet sln add apphost/«ProjectName».AppHost.csprojAlso add every .csproj under services/shared/. Use--solution-folder to place them directly in the desired folder —
without it, dotnet sln add creates nested <Folder> elements in.slnx which are invalid (the schema only allows <Folder> as
direct children of <Solution>):
dotnet sln add services/shared/ServiceDefaults/ServiceDefaults.csproj \
services/shared/Database/Database.csproj \
services/shared/Database.Abstractions/Database.Abstractions.csproj \
services/shared/Exceptions/Exceptions.csproj \
--solution-folder /services/shared/The dotnet new sln command produces a .slnx file (XML-based
solution format) on .NET 10+. Either .sln or .slnx is fine.
2h. Build to confirm:
dotnet build apphostVerify:
apphost/«ProjectName».AppHost.csprojexists with<Sdk Name="Aspire.AppHost.Sdk" .../>.apphost/Program.csexists withDistributedApplication.CreateBuilder.apphost/aspire.config.jsonexists withappHost.pathpointing at
the.csproj.«ProjectName».slnx(or.sln) exists at the repo root.- No
#:sdkdirective in any.csfile.
The CLI may also create apphost/.agents/ and apphost/.vscode/.
Leave them as-is.
Step 3 — Prepare shared libraries
Load references/prepare-shared-libs.md and follow it. This step
updates the pre-existing services/shared/ projects to match the new
project name and ensures all NuGet package versions are current.
Summary of what the reference prescribes:
- Find all
.csprojfiles underservices/shared/. - Replace the template namespace prefix in
<RootNamespace>and<InternalsVisibleTo>elements with«ProjectName». - Find all
.csfiles underservices/shared/and replace the
template namespace prefix innamespacedeclarations andusing
directives with«ProjectName». - For each
<PackageReference>, query for the latest stable version
and update it in place. - Build all shared projects to confirm they compile.
Step 4 — Add .NET gRPC services
Read and follow .cursor/add-dotnet-service/SKILL.md for each of
the three bootstrap services: auth, payments, api.
For each service, supply these inputs:
| Service name | PascalCase | Envoy prefix |
|---|---|---|
auth |
Auth |
/auth/ |
payments |
Payments |
/payments/ |
api |
Api |
/api/ |
Since proxy/ does not exist yet at this point, the sub-skill will
skip Envoy updates — Step 7 creates the full Envoy config from scratch
for all services at once.
The sub-skill handles dotnet sln add for each service (Step 8).
After all three, verify the AppHost builds:
dotnet build apphostStep 5–6 — Add Angular admin client + proto codegen
Read and follow .cursor/add-angular-client/SKILL.md for theadmin client.
Supply these inputs:
- Client name:
admin - Protos: all services (
auth,payments,api) — proto files
live inside each API project atservices/«servicename»/src/«ProjectName».«ServiceName».Api/Protos/ - Envoy route (for the client itself):
/(catch-all) - gRPC transport
«routeprefix»:api— the Envoy prefix for
the backend the Greeter calls. Not the client's own route
above. Without it,/greet.Greeter/SayHellomatches the catch-all/→ Angular SSR → 404 UNIMPLEMENTED. With/api, the path
becomes/api/greet.Greeter/SayHello→ Envoy prefix-rewrites →
gRPC backend.
The sub-skill scaffolds the Angular SSR project, installs Tailwind
CSS v4, patches npm scripts for Aspire's PORT and 0.0.0.0
binding, createsapphost/ClientApp/ClientAppResourceBuilderExtensions.cs with the
reusable AddClientApp (dev server) and AddClientHost (unified SSR
host) extension methods, registers the client in apphost/Program.cs
via proxy.WithClient(...) + builder.AddClientApp(...), and wires
buf + Connect proto codegen for all three services.
During bootstrap only: also follow the Greeter example inadd-angular-client/references/proto-codegen.md (the "Example:
wiring a Greeter component" section) to prove end-to-end gRPC-Web
connectivity in the smoke check.
Since proxy/ does not exist yet, the sub-skill will skip Envoy
updates — Step 7 creates the full Envoy config.
Step 7 — Envoy proxy + unified SSR host
Load references/envoy-proxy.md and follow Steps 6a–6l. Creates
the proxy/ directory with a Dockerfile, fragment templates
(envoy.yaml.tmpl base, envoy.listener.yaml.tmpl,envoy.rds.yaml.tmpl, envoy.vhost.yaml.tmpl,envoy.cluster.yaml.tmpl), and an entrypoint.sh that renders the
config per ENVOY_MODE (dev = one listener per client routing to
its dev server; dev-host/publish = routing to the unified SSR
host with x-client headers). Each listener pair terminates TLS on
TCP (HTTP/2 + HTTP/1.1) and UDP (HTTP/3 QUIC) using Aspire's
developer certificate. Also creates the unified SSR host underclients/host/ (Express dispatcher + multi-client Dockerfile) and a
repo-root .dockerignore. Addsapphost/EnvoyProxy/EnvoyProxyResourceBuilderExtensions.cs
(AddEnvoyProxy, WithClient, WithUpstreamEndpoint) and theAddClientHost method, and registers everything inapphost/Program.cs. The reference includes the full Program.cs at
this point.
Step 8 — Update README
Append a "Running the app" section to README.md. Do not remove or
reorder existing content.
## Running the app
The whole stack (Aspire AppHost + .NET gRPC services + Angular admin client +
Envoy proxy) is started via the Aspire CLI from the repo root:
```bash
aspire run
```
This launches:
- Envoy front proxy (proxies all traffic; ports assigned by Aspire)
- Angular `admin` client with SSR (proxied via Envoy at `/`)
- .NET gRPC services from `services/` (proxied via Envoy):
- `auth` at `/auth/*`
- `payments` at `/payments/*`
- `api` at `/api/*`
- The Aspire dashboard (URL printed in the terminal on startup)
All resource URLs (including Envoy) are shown in the Aspire dashboard.
`clients/app/` is reserved for an end-user Angular client and is not yet
scaffolded.
Stop everything with `Ctrl+C`, or from another shell:
```bash
aspire stop
```Optionally include a ## Layout block (the diagram from Step 1).
Step 9 — Smoke check
Launch. Use aspire start (background) not aspire run
(foreground-blocking). Re-source the version manager in the launching
shell (principle 3). From the repo root:
if [ -s "$HOME/.nvm/nvm.sh" ]; then
export NVM_DIR="$HOME/.nvm" && \. "$NVM_DIR/nvm.sh"
fi
aspire start --non-interactive --format JsonCapture the dashboard URL from the JSON output. Wait ~20–30 s fornpm install, dotnet builds, and the Envoy image pull.
Inventory. Run aspire describe --format Json and verify:
| Resource | Type | Expected state | Notes |
|---|---|---|---|
admin-installer |
Executable | Finished, exit 0 | Auto-npm install for the JS app |
admin |
Executable | Running, Healthy | npm start = proto codegen + ng serve (port is Aspire-assigned) |
auth |
Project | Running, Healthy | .NET gRPC service (port is Aspire-assigned) |
payments |
Project | Running, Healthy | .NET gRPC service (port is Aspire-assigned) |
api |
Project | Running, Healthy | .NET gRPC service (port is Aspire-assigned) |
envoy |
Container | Running, Healthy | front proxy (ports are Aspire-assigned) |
Six rows corresponding to six .Add... calls in Program.cs
(principle 6).
Curl checks. Obtain the Envoy admin URL and the admin client's
listener URL (the admin-web endpoint on the envoy resource, e.g.https://localhost:20000) from aspire describe --format Json. The
listener uses a self-signed Aspire developer certificate, so pass-k (allow insecure) on all HTTPS checks. All pass on a clean run:
| # | Check | Command | Pass criterion |
|---|---|---|---|
| 1 | Envoy admin UI | curl -sI <envoy-admin-url>/ |
200 |
| 2 | Envoy to admin | curl -ksI <admin-web-url>/ |
200, text/html |
| 3 | SSR rendered | curl -ks <admin-web-url>/ | head -c 1200 |
Rendered Angular markup |
| 4 | Envoy to /auth/ |
curl -ksi -X POST -H 'content-type: application/grpc-web' <admin-web-url>/auth/ |
non-5xx with grpc-status header |
| 5 | Envoy to /payments/ |
same with /payments/ |
as #4 |
| 6 | Envoy to /api/ |
same with /api/ |
as #4 |
If any check fails, stop and consultreferences/cleanup-and-troubleshooting.md — the Appendix maps
symptoms back to the rule (A–C) the failure violates. Don't silently
work around.
Step 10 — Cleanup / reset
Load references/cleanup-and-troubleshooting.md and follow
Steps 9a–9d. Stops the AppHost, cleans orphan DCP containers,
confirms ports are free, and documents recovery from AppHost
compilation errors.
Skill maintenance
Rules the skill author (not the agent running it) follows when
editing this file:
- Filesystem-path agnosticism. No absolute paths
(/home/<user>/...,/Users/<user>/...,C:\Users\...\) in the
skill or its prescribed outputs. Use repo-relative paths or$HOME
expansion. - External-repo independence. Patterns are described inline
in enough detail to implement without consulting any
other repository. Links to external code are aids, never
prerequisites. - Prescribe, don't narrate. Action steps state what to do, with
the minimum context to do it right. Symptoms and failure modes
live in the Appendix, which the agent reads only when something
unexpected happens. - Ship a working configuration. If a step has a "simpler MVP"
that produces known-broken behavior plus a "real fix" later, the
skill teaches only the real fix. There is no "documented caveat"
middle category in Step 9. - Update before the next step. When a generator output, flag, or
API drifts, update this file as part of the bootstrap, before
moving on. The skill is the next agent's contract. - Keep SKILL.md under 500 lines. Detailed implementation content
belongs inreferences/files, loaded on demand. SKILL.md carries
the workflow structure, principles, and critical inline steps.
Guardrails
- Never run destructive commands (
rm -rf,git reset --hard, force
pushes). - Never commit on the user's behalf during bootstrap unless asked.
- Never invent MCP tool argument names — list the server's tools
first; if not populated, use the CLI. - Keep generator output inside the agreed folders (Step 1). If a tool
insists on a different layout, stop and re-confirm with the user. - Never touch another project's resources without explicit user
confirmation. Step 0 surfaces them; Step 10 cleans them only after
asking. - The Aspire CLI typically lives at
$HOME/.aspire/bin/aspire. Ifaspireis not onPATH, prepend that directory or invoke the
absolute path; do not reinstall it.