dotnet-starter-kit Documentation

Production Grade Cloud-Ready .NET 10 Starter Kit (Web API + React Client) with Multitenancy Support, and Clean/Modular Architecture that saves roughly 200+...

RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy dotnet-starter-kit on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name dotnet-starter-kit -p 9000:9000 --restart always postgres:18-alpine
Configured Services & Ports
βš™οΈ postgres
image: postgres:18-alpine
βš™οΈ redis
image: valkey/valkey:9.1.0-alpine
βš™οΈ rustfs
image: rustfs/rustfs:1.0.0
πŸ”Œ Ports: ${FSH_S3_PORT:-9000}:9000
βš™οΈ rustfs-init
image: amazon/aws-cli:2.37.3
βš™οΈ mailpit
image: axllent/mailpit:v1.31.3
πŸ”Œ Ports: 127.0.0.1:${FSH_MAILPIT_PORT:-8025}:8025
βš™οΈ migrator
image: fsh/dbmigrator:local
βš™οΈ api
image: fsh/api:local
πŸ”Œ Ports: ${FSH_API_PORT:-8080}:8080
βš™οΈ admin
image: fsh/admin:local
πŸ”Œ Ports: ${FSH_ADMIN_PORT:-8081}:80
βš™οΈ dashboard
image: fsh/dashboard:local
πŸ”Œ Ports: ${FSH_DASHBOARD_PORT:-8082}:80
docker-compose.yml
yaml (docker-compose.yml)
# fullstackhero β€” production docker-compose.
# Run from this directory:
#   cp .env.example .env && $EDITOR .env
#   docker compose up -d --build
#
# Operator owns the edge (TLS / subdomain routing). This file publishes
# the FSH services and the RustFS S3 API on host ports (browsers upload
# straight to presigned S3 URLs); postgres/redis stay compose-internal
# unless you uncomment their ports blocks.
# The Mailpit inbox UI is published too, bound to the host loopback only.

name: fsh

services:
  postgres:
    image: postgres:18-alpine
    container_name: fsh-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: fsh
      POSTGRES_USER: fsh
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
    volumes:
      # The 18+ images keep PGDATA in a versioned subdirectory (/var/lib/postgresql/18/docker) and
      # refuse to start with a mount at the old /var/lib/postgresql/data, even on an empty volume.
      - pg_data:/var/lib/postgresql
      - ./postgres-init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U fsh -d fsh"]
      interval: 5s
      timeout: 3s
      retries: 12
    # Uncomment to expose for host psql / pg_dump access:
    # ports:
    #   - "5432:5432"

  redis:
    image: valkey/valkey:9.1.0-alpine
    container_name: fsh-redis
    restart: unless-stopped
    command:
      - valkey-server
      - --requirepass
      - ${REDIS_PASSWORD:?REDIS_PASSWORD is required}
      - --appendonly
      - "yes"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD-SHELL", "valkey-cli -a $$REDIS_PASSWORD PING | grep PONG"]
      interval: 5s
      timeout: 3s
      retries: 12
    environment:
      REDIS_PASSWORD: ${REDIS_PASSWORD}
    # Uncomment to expose for host redis-cli:
    # ports:
    #   - "6379:6379"

  rustfs:
    image: rustfs/rustfs:1.0.0
    container_name: fsh-rustfs
    restart: unless-stopped
    environment:
      RUSTFS_ACCESS_KEY: ${RUSTFS_ACCESS_KEY:?RUSTFS_ACCESS_KEY is required}
      RUSTFS_SECRET_KEY: ${RUSTFS_SECRET_KEY:?RUSTFS_SECRET_KEY is required}
      RUSTFS_CONSOLE_ENABLE: "true"
      # The admin and dashboard PUT uploads cross-origin to presigned URLs on this store.
      RUSTFS_CORS_ALLOWED_ORIGINS: ${FSH_ADMIN_URL},${FSH_DASHBOARD_URL}
    volumes:
      - rustfs_data:/data
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:9000/health"]
      interval: 10s
      timeout: 3s
      retries: 12
    # The S3 API is published because browsers reach it directly through the
    # presigned URLs the API hands out (FSH_S3_PUBLIC_URL). This binds all
    # interfaces: firewall the port, or when your proxy runs on this host bind
    # it to loopback instead ("127.0.0.1:${FSH_S3_PORT:-9000}:9000") so only
    # the proxy can reach it. Uncomment the console (admin UI) line to expose it too:
    ports:
      - "${FSH_S3_PORT:-9000}:9000"   # S3 API
    #   - "9001:9001"   # Web console

  # One-shot: create the bucket the Files module writes to and open public/*
  # to anonymous reads. The app never auto-creates the bucket (S3StorageService
  # just PutObjects into Storage:S3:Bucket), so without this the first upload
  # fails with NoSuchBucket. Visibility lives in the object key: public files
  # go under public/ and are served straight from ${FSH_S3_PUBLIC_URL}/fsh/...;
  # everything else (private/, and keys from before public/ existed) needs a
  # presigned URL. Re-applied on every `up`, so it also narrows older policies.
  rustfs-init:
    image: amazon/aws-cli:2.37.3
    container_name: fsh-rustfs-init
    restart: "no"
    depends_on:
      rustfs: { condition: service_healthy }
    environment:
      AWS_ACCESS_KEY_ID: ${RUSTFS_ACCESS_KEY}
      AWS_SECRET_ACCESS_KEY: ${RUSTFS_SECRET_KEY}
      AWS_DEFAULT_REGION: us-east-1
    entrypoint: /bin/sh
    command:
      - -c
      - |
        set -e
        aws --endpoint-url http://rustfs:9000 s3api head-bucket --bucket fsh 2>/dev/null ||
          aws --endpoint-url http://rustfs:9000 s3api create-bucket --bucket fsh
        aws --endpoint-url http://rustfs:9000 s3api put-bucket-policy --bucket fsh --policy '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::fsh/public/*"]}]}'

  # Local mail catcher, started only with the mail-catcher profile
  # (COMPOSE_PROFILES in .env). SMTP (1025) stays on the compose network.
  mailpit:
    image: axllent/mailpit:v1.31.3
    container_name: fsh-mailpit
    restart: unless-stopped
    profiles: [mail-catcher]
    ports:
      # Loopback only: the inbox holds live password-reset and confirmation links.
      - "127.0.0.1:${FSH_MAILPIT_PORT:-8025}:8025"

  migrator:
    build:
      context: ../..
      dockerfile: src/Host/FSH.Starter.DbMigrator/Dockerfile
    image: fsh/dbmigrator:local
    container_name: fsh-migrator
    restart: "no"
    depends_on:
      postgres: { condition: service_healthy }
    environment:
      DOTNET_ENVIRONMENT: Production
      # The chiseled image has no libgssapi_krb5; Npgsql's default GSS Prefer logs a load failure per start.
      DatabaseOptions__ConnectionString: Host=postgres;Database=fsh;Username=fsh;Password=${POSTGRES_PASSWORD};GSS Encryption Mode=Disable
      CachingOptions__Redis: redis,password=${REDIS_PASSWORD}
      JwtOptions__SigningKey: ${JWT_SIGNING_KEY:?JWT_SIGNING_KEY is required}
      Seed__DefaultAdminPassword: ${SEED_ADMIN_PASSWORD:?SEED_ADMIN_PASSWORD is required}
    command: ["apply", "--seed"]

  api:
    build:
      context: ../..
      dockerfile: src/Host/FSH.Starter.Api/Dockerfile
    image: fsh/api:local
    container_name: fsh-api
    restart: unless-stopped
    depends_on:
      postgres:   { condition: service_healthy }
      redis:      { condition: service_healthy }
      rustfs:      { condition: service_healthy }
      rustfs-init: { condition: service_completed_successfully }
      migrator:   { condition: service_completed_successfully }
    environment:
      DOTNET_ENVIRONMENT: Production
      ASPNETCORE_URLS: http://+:8080
      # Same chiseled-image reason as the migrator: no libgssapi_krb5, so skip the GSS attempt.
      DatabaseOptions__ConnectionString: Host=postgres;Database=fsh;Username=fsh;Password=${POSTGRES_PASSWORD};GSS Encryption Mode=Disable
      CachingOptions__Redis: redis,password=${REDIS_PASSWORD}
      JwtOptions__SigningKey: ${JWT_SIGNING_KEY}
      Seed__DefaultAdminPassword: ${SEED_ADMIN_PASSWORD}
      Storage__Provider: s3
      Storage__S3__ServiceUrl: http://rustfs:9000
      # Presigned upload/download URLs go to browsers, which cannot resolve rustfs:9000.
      Storage__S3__PresignServiceUrl: ${FSH_S3_PUBLIC_URL:?FSH_S3_PUBLIC_URL is required}
      # Durable URLs for public files (avatars, product images): path-style
      # //public/..., readable through the public/* policy.
      Storage__S3__PublicBaseUrl: ${FSH_S3_PUBLIC_URL}/fsh
      Storage__S3__AccessKey: ${RUSTFS_ACCESS_KEY}
      Storage__S3__SecretKey: ${RUSTFS_SECRET_KEY}
      Storage__S3__Bucket: fsh
      Storage__S3__ForcePathStyle: "true"
      # Defaults target the bundled Mailpit (plain SMTP, no STARTTLS); a real provider is set in .env only.
      MailOptions__Smtp__Host: ${FSH_SMTP_HOST:-mailpit}
      MailOptions__Smtp__Port: ${FSH_SMTP_PORT:-1025}
      MailOptions__Smtp__Security: ${FSH_SMTP_SECURITY:-None}
      MailOptions__Smtp__UserName: ${FSH_SMTP_USERNAME:-}
      MailOptions__Smtp__Password: ${FSH_SMTP_PASSWORD:-}
      # appsettings.Production.json leaves the sender blank, and a real provider rejects an empty MAIL FROM.
      MailOptions__From: ${FSH_MAIL_FROM:[email protected]}
      AllowedHosts: "*"
      HangfireOptions__UserName: ${HANGFIRE_USERNAME:?HANGFIRE_USERNAME is required}
      HangfireOptions__Password: ${HANGFIRE_PASSWORD:?HANGFIRE_PASSWORD is required}
      CorsOptions__AllowedOrigins__0: ${FSH_ADMIN_URL:?FSH_ADMIN_URL is required}
      CorsOptions__AllowedOrigins__1: ${FSH_DASHBOARD_URL:?FSH_DASHBOARD_URL is required}
      # E-mail-link origins. Separate list from CORS: the API origin must stay off it.
      FrontendOptions__AllowedOrigins__0: ${FSH_ADMIN_URL}
      FrontendOptions__AllowedOrigins__1: ${FSH_DASHBOARD_URL}
      FrontendOptions__DefaultOrigin: ${FSH_DASHBOARD_URL}
      OpenTelemetryOptions__Exporter__Otlp__Endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:-}
    ports:
      - "${FSH_API_PORT:-8080}:8080"

  admin:
    build:
      context: ../../clients/admin
    image: fsh/admin:local
    container_name: fsh-admin
    restart: unless-stopped
    depends_on:
      migrator: { condition: service_completed_successfully }
    environment:
      FSH_API_URL: ${FSH_API_URL:?FSH_API_URL is required}
      FSH_DASHBOARD_URL: ${FSH_DASHBOARD_URL}
      FSH_DEFAULT_TENANT: ${FSH_DEFAULT_TENANT:-root}
    ports:
      - "${FSH_ADMIN_PORT:-8081}:80"

  dashboard:
    build:
      context: ../../clients/dashboard
    image: fsh/dashboard:local
    container_name: fsh-dashboard
    restart: unless-stopped
    depends_on:
      migrator: { condition: service_completed_successfully }
    environment:
      FSH_API_URL: ${FSH_API_URL}
      FSH_DEFAULT_TENANT: ${FSH_DEFAULT_TENANT:-root}
    ports:
      - "${FSH_DASHBOARD_PORT:-8082}:80"

volumes:
  pg_data:
  redis_data:
  rustfs_data:

1. Project Overview & Quickstart (fullstackhero/dotnet-starter-kit)

File: README.md


Why FullStackHero?

Most starter kits give you a login page and a TODO list. This one gives you the boring, hard parts already done right β€” multitenancy, auth, billing, auditing, background jobs, real-time, file storage, observability β€” across a clean Vertical Slice backend and two polished React 19 front-ends, orchestrated locally with one command via .NET Aspire, and deployable to Docker or AWS.

You scaffold with the fsh CLI and get the complete, detached source β€” every BuildingBlock, Module, and Host project with real project references. No hidden NuGet runtime, nothing to "eject" later. Own it, read it, change it.

bash
dotnet tool install -g FullStackHero.CLI
fsh new MyApp
cd MyApp
dotnet run --project src/Host/MyApp.AppHost   # πŸŽ‰ whole stack up: API + 2 React apps + Postgres + Valkey + MinIO

Then open the Aspire dashboard at https://localhost:15888, the API + Scalar docs at https://localhost:7030/scalar, the admin app at http://localhost:5173, and the dashboard app at http://localhost:5174. Sign in with a seeded demo account (e.g. [email protected] / Password123!).


✨ What's inside

Backend β€” modular monolith, vertical slices

  • .NET 10 Β· C# latest Β· Minimal APIs Β· Mediator (source-generated CQRS) Β· FluentValidation
  • EF Core 10 on PostgreSQL (Npgsql), with domain events, the specification pattern, soft-delete + audit interceptors, and tenant-isolated DbContexts.
  • JWT auth + ASP.NET Identity β€” issuance/refresh, roles & fine-grained permissions, rate-limited auth, password policies, sessions, impersonation.
  • Multitenancy via Finbuckle β€” tenant resolution, provisioning, per-tenant migrations & seeding, isolation enforced by default.
  • Cross-cutting: HybridCache on Valkey (Redis-compatible), Hangfire jobs, presigned S3/MinIO storage, mailing, idempotency, quotas, rate limiting, API versioning, RFC 9457 ProblemDetails.
  • Observability: Serilog structured logging + OpenTelemetry traces/metrics/logs, health probes, security/exception auditing.
  • Docs: OpenAPI + the Scalar API reference UI.

Front-ends β€” two React 19 apps

  • clients/admin (operator console) and clients/dashboard (tenant app): React 19 + Vite 7 + TypeScript, TanStack Query v5, React Router 7, Radix + Tailwind v4 (shadcn-style), real-time via SignalR/SSE.
  • Runtime config (/config.json, no rebuild per environment), hand-written typed API client, and Playwright E2E suites.

Modules (bounded contexts)

Identity Β· Multitenancy Β· Billing Β· Catalog Β· Tickets Β· Chat Β· Files Β· Webhooks Β· Auditing Β· Notifications β€” each a runtime project plus a .Contracts project (its only public surface), boundaries enforced by architecture tests.

Cloud-native & DevOps

  • .NET Aspire orchestrates the entire stack locally with one command (Postgres + pgAdmin, Valkey + RedisInsight, MinIO, migrator, demo-seeder, API, and both React apps).
  • Docker Compose production stack (deploy/docker) and Terraform for AWS (deploy/terraform); API image published to GHCR.
  • A one-shot DbMigrator (migrations are never run at API startup), and the fsh CLI + dotnet new template for distribution.

Quality

1,600+ backend tests (xUnit, Shouldly, NSubstitute, AutoFixture, NetArchTest boundaries, Testcontainers integration) and 200+ front-end E2E tests (Playwright). Path-scoped CI for backend and frontend; warnings-as-errors.


πŸš€ Getting started

bash
dotnet tool install -g FullStackHero.CLI
fsh doctor          # verify your environment (SDK, Docker, Aspire, ports)
fsh new MyApp       # interactive wizard

The wizard asks what to include (Aspire AppHost, the React apps). Non-interactive:

bash
fsh new MyApp --non-interactive          # full stack, Postgres
fsh new MyApp --no-frontend              # backend-only
fsh new MyApp --no-aspire --no-frontend  # minimal API + migrator

Option 2 β€” the `dotnet new` template

bash
dotnet new install FullStackHero.NET.StarterKit
dotnet new fsh -n MyApp

Option 3 β€” clone the repo

bash
git clone https://github.com/fullstackhero/dotnet-starter-kit.git MyApp && cd MyApp
dotnet run --project src/Host/FSH.Starter.AppHost

Prerequisites: .NET 10 SDK Β· Docker (Postgres/Valkey/MinIO via Aspire) Β· Node 20+ (for the React apps).

fsh commands: new Β· doctor Β· info Β· update Β· --version. Full reference β†’ fullstackhero.net/docs/cli.


🧱 Tech stack

Backend Frontend
Runtime .NET 10 / C# latest Framework React 19 + Vite 7 + TS 5
API Minimal APIs + Mediator (CQRS) Data TanStack Query v5
Validation FluentValidation Routing React Router 7
ORM / DB EF Core 10 / PostgreSQL UI Radix + Tailwind v4 (shadcn)
Auth JWT + ASP.NET Identity Realtime SignalR Β· SSE
Multitenancy Finbuckle 10 Tests Playwright
Cache / Jobs Valkey Β· Hangfire
Storage S3 / MinIO (presigned) Infra
Docs OpenAPI + Scalar Orchestration .NET Aspire
Observability Serilog + OpenTelemetry Deploy Docker Compose Β· Terraform
Testing xUnit Β· Testcontainers Β· NetArchTest

πŸ—ΊοΈ Repository layout

Path What
src/BuildingBlocks/ Shared framework libraries (Core, Persistence, Web, Caching, Eventing, Storage, Quota…)
src/Modules/{Name}/ Bounded contexts β€” each with a runtime project + a .Contracts project (its public API)
src/Host/FSH.Starter.Api Composition-root Web API host
src/Host/FSH.Starter.AppHost .NET Aspire orchestrator (Postgres, Valkey, MinIO, migrator, API, both React apps)
src/Host/FSH.Starter.DbMigrator One-shot migrate/seed runner (DB is not migrated at API startup)
src/Tools/CLI The fsh CLI (Spectre.Console)
clients/admin, clients/dashboard The two React apps
deploy/ Docker Compose, Terraform (AWS), Dokploy
src/Tests/ Unit, Architecture (NetArchTest), Integration (Testcontainers)

Architecture deep-dive β†’ fullstackhero.net/docs/architecture.


☁️ Deploy

Single-host via Docker Compose:

bash
cd deploy/docker
cp .env.example .env   # fsh new pre-generates this with strong secrets
docker compose up -d --build

AWS via Terraform (ECS Fargate + RDS + ElastiCache + S3/CloudFront) lives in deploy/terraform.

Guides β†’ Local orchestration Β· Docker Β· AWS / Terraform Β· Database migrations.


πŸ§ͺ Testing

bash
dotnet test src/FSH.Starter.slnx        # backend: unit + architecture + Testcontainers integration
cd clients/admin     && npm run test:e2e # Playwright (operator app)
cd clients/dashboard && npm run test:e2e # Playwright (tenant app)

Integration tests require Docker (Testcontainers spins real Postgres). Architecture tests enforce module boundaries.


πŸ“– Documentation

Full guides, module references, and architecture decisions live at fullstackhero.net:


🀝 Contributing

Issues and PRs are welcome β€” see CONTRIBUTING.md. Branch from and target main; CI runs path-scoped backend + frontend pipelines, and stable releases are cut from v* tags.

πŸ“„ License

MIT β€” see LICENSE. Built and maintained by Mukesh Murugan and the FullStackHero community, for teams that want to ship fast without sacrificing architectural discipline.


File: README-template.md

FSH.Starter

Your application, generated from the FSH .NET Starter Kit β€” a production-ready modular
.NET 10 monolith with two React 19 apps, multitenancy, identity, background jobs, and
cloud-native deploy.

You own all of this source. There are no framework NuGet packages to track or upgrade β€”
the shared code lives in src/BuildingBlocks and is yours to change.

Prerequisites

Quick start

bash
dotnet run --project src/Host/FSH.Starter.AppHost

Aspire starts Postgres, Redis, and MinIO, runs database migrations, then launches the API
and both React apps.

Surface URL
Aspire dashboard https://localhost:15888
API + Scalar docs https://localhost:7030/scalar
Admin console http://localhost:5173
Tenant dashboard http://localhost:5174

Backend only

bash
dotnet run --project src/Host/FSH.Starter.Api      # needs external Postgres + Redis

Frontend only (against a running API)

bash
cd clients/admin     && npm install && npm run dev   # β†’ http://localhost:5173
cd clients/dashboard && npm install && npm run dev   # β†’ http://localhost:5174

The React apps read their API URL at runtime from public/config.json β€” no rebuild to repoint.

Project structure

text
src/
  BuildingBlocks/      Shared framework libraries β€” yours to modify
  Modules/             Bounded contexts: Identity, Multitenancy, Auditing, Billing,
                       Catalog, Chat, Files, Notifications, Tickets, Webhooks
  Host/
    FSH.Starter.Api/                    API composition root
    FSH.Starter.AppHost/                .NET Aspire orchestrator
    FSH.Starter.DbMigrator/             One-shot migrate / seed runner
    FSH.Starter.Migrations.PostgreSQL/  EF Core migrations
  Tests/               Unit, integration (Testcontainers), and architecture tests
clients/
  admin/               Operator console (React 19 + Vite + Tailwind)
  dashboard/           Tenant app (React 19 + Vite + Tailwind, SSE live feed)
deploy/
  docker/              Production docker-compose + .env
  terraform/           AWS infrastructure (ECS, RDS, ElastiCache, S3)

Database

Migrations run automatically under Aspire. To apply them yourself:

bash
dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply --seed

Make it yours β€” first-run checklist

This project shipped with sensible defaults. Before production:

  • Secrets β€” set strong values in deploy/docker/.env (the fsh CLI generates these
    for you; otherwise cp deploy/docker/.env.example deploy/docker/.env and fill them in).
    Never commit .env.
  • Logo β€” replace clients/admin/public/logo-fullstackhero.png and
    clients/dashboard/public/logo-fullstackhero.png with your own.
  • Mail β€” configure SMTP / SendGrid under MailOptions in
    src/Host/FSH.Starter.Api/appsettings.json.
  • OpenAPI contact β€” update OpenApiOptions.Contact in appsettings.json.
  • Container registry & infra β€” set your registry and review bucket / database names
    in deploy/terraform/apps/starter/**/variables.tf and terraform.tfvars.

Production (Docker Compose)

bash
cd deploy/docker
# .env is generated for you by the fsh CLI; otherwise: cp .env.example .env && edit
docker compose up -d --build

Sign in to the admin console as [email protected] using the SEED_ADMIN_PASSWORD from your
.env, then rotate it from Settings β†’ Security.

Adding a feature

  1. Contracts command/query in src/Modules/{Module}.Contracts/v1/{Area}/{Feature}/
  2. Handler + FluentValidation validator in src/Modules/{Module}/Features/...
  3. Endpoint, wired into the module's MapEndpoints()
  4. Tests

Running tests

bash
dotnet test src/FSH.Starter.slnx       # integration tests require Docker

Learn more

2. Official Technical Reference & Guides (fullstackhero/docs)

FullStackHero Docs

Documentation site for the FullStackHero .NET starter kit. Built with Astro 6 and Tailwind 4.

Local development

bash
cd docs
npm install
npm run dev

The site runs on http://localhost:4321. Edit MDX files under src/content/docs/ and the dev server hot-reloads. Note that pagefind search is built only by npm run build, so the search modal will report "index not available" during dev.

Build

bash
npm run build      # astro build && pagefind --site dist
npm run preview    # serves dist/ on a local port

Deploy

Cloudflare Pages, configured by wrangler.toml. Production branch: main. Build command for the Pages dashboard:

text
npm ci && npm run build

Output directory: dist.

Content authoring

Add a new doc page by creating an MDX file under src/content/docs/<section>/. Frontmatter shape is defined by src/content.config.ts:

yaml
---
title: Page title
sidebar:
  label: Optional override of title in sidebar
  order: 10                    # lower = higher in section
  hidden: false                # exclude from sidebar but still routable
pageType: guide                # guide | reference | concept | recipe
---

The sidebar tree is derived from file structure. Section labels and order live in src/content/docs/_sections.ts.

Design system

Tokens, prose styles, and code-block themes are forked verbatim from codewithmukesh/blog (see provenance comments in src/styles/*.css). Brand primary is #15803d (green-700); the user-facing brand green #16a34a lives in --primary-soft so it surfaces in accent text, tinted backgrounds, and the gradient mid-stop.

Pagefind. Index is generated as a post-build step over the dist/ HTML. Only /docs/* pages are indexed (scoped via data-pagefind-body on the docs <article>). Trigger via Ctrl/Cmd+K.