diff --git a/src/content/docs/ai-development/claude-code.mdx b/src/content/docs/ai-development/claude-code.mdx index 9d6d6ee3..2e3990b7 100644 --- a/src/content/docs/ai-development/claude-code.mdx +++ b/src/content/docs/ai-development/claude-code.mdx @@ -1,7 +1,7 @@ --- title: Developing with Claude Code description: A practical guide to building on fullstackhero with Claude Code - how it loads AGENTS.md, invokes skills, runs review workflows, and the build/test loop to follow. -lastUpdated: 2026-05-24 +lastUpdated: 2026-09-25 sidebar: label: Developing with Claude Code order: 3 @@ -67,12 +67,12 @@ dotnet test src/FSH.Starter.slnx # unit + architecture + integration ``` -`Integration.Tests` spins up real Postgres, Valkey, and MinIO via Testcontainers. If Docker isn't running +`Integration.Tests` spins up real Postgres, Valkey, and RustFS via Testcontainers. If Docker isn't running you'll see `DockerUnavailableException` - that's environmental, not a code regression. Run the unit projects (e.g. `dotnet test src/Tests/Catalog.Tests`) to validate logic without Docker. -To see it all running, the Aspire AppHost brings up the whole stack - Postgres, Valkey, MinIO, the migrator, +To see it all running, the Aspire AppHost brings up the whole stack - Postgres, Valkey, RustFS, the migrator, the API, and both React apps - with one command: ```bash diff --git a/src/content/docs/building-blocks/storage.mdx b/src/content/docs/building-blocks/storage.mdx index bda6134c..ec7c572a 100644 --- a/src/content/docs/building-blocks/storage.mdx +++ b/src/content/docs/building-blocks/storage.mdx @@ -1,7 +1,7 @@ --- title: Storage building block -lastUpdated: 2026-06-11 -description: File storage abstraction - local filesystem or S3-compatible (MinIO / AWS S3) - with presigned URLs, tenant isolation, and optional quota metering. +lastUpdated: 2026-09-25 +description: File storage abstraction - local filesystem or S3-compatible (AWS S3, RustFS, MinIO, R2...) - with presigned URLs, tenant isolation, and optional quota metering. sidebar: label: Storage order: 10 @@ -12,10 +12,10 @@ seo: keywords: '.net s3 storage, minio dotnet, presigned url .net, aws sdk dotnet storage, tenant scoped file paths' --- -The Storage block is the kit's blob-storage abstraction. One interface (`IStorageService`), two implementations - `LocalStorageService` (filesystem) and `S3StorageService` (AWS S3 + MinIO compatible) - with presigned URL support and an optional `QuotaMeteredStorageService` decorator that meters per-tenant usage against the Quota block. +The Storage block is the kit's blob-storage abstraction. One interface (`IStorageService`), two implementations - `LocalStorageService` (filesystem) and `S3StorageService` (AWS S3 and any S3-compatible store - the kit runs RustFS locally) - with presigned URL support and an optional `QuotaMeteredStorageService` decorator that meters per-tenant usage against the Quota block. -Clients PUT directly to S3 / MinIO using a short-lived presigned URL minted by your API. Your API never proxies bytes. Same on the read side - `GenerateDownloadUrlAsync` mints a presigned GET URL the browser hits directly. +Clients PUT directly to S3 using a short-lived presigned URL minted by your API. Your API never proxies bytes. Same on the read side - `GenerateDownloadUrlAsync` mints a presigned GET URL the browser hits directly. ## What it ships @@ -56,7 +56,7 @@ public interface IStorageService ### Implementations - **`LocalStorageService`** - stores under `wwwroot/uploads/{owner-type}/{guid}_{sanitized-filename}` and validates extension + size against `FileTypeMetadata.GetRules(fileType)`. Presigning is a **dev-only token fallback**: `GenerateUploadUrlAsync` issues a `local://upload/{token}` URL backed by `LocalPresignTokenStore`; `GenerateDownloadUrlAsync` and `BuildPublicUrl` return server-relative `/uploads/...` paths (no signing needed). -- **`S3StorageService`** - uses `AWSSDK.S3` with a singleton `IAmazonS3` client. A custom `ServiceUrl` (MinIO etc.) switches to path-style addressing per `ForcePathStyle`; presigned PUT/GET URLs come from the SDK's request signer. +- **`S3StorageService`** - uses `AWSSDK.S3` with a singleton `IAmazonS3` client. A custom `ServiceUrl` (RustFS, MinIO, etc.) switches to path-style addressing per `ForcePathStyle`; presigned PUT/GET URLs come from the SDK's request signer. - **`QuotaMeteredStorageService`** - decorator. `CheckAndRecordAsync(tenantId, QuotaResource.StorageBytes, bytes, ct)` on upload (throws 507 when exceeded, rolls back the charge if the write fails); refunds the object's size on `RemoveAsync`. Requests with no resolved tenant pass through unmetered. ### Request / response @@ -69,7 +69,7 @@ public interface IStorageService ### Options -- **`S3StorageOptions`** - `Bucket`, `Region`, `Prefix`, `PublicRead` (default true), `PublicBaseUrl` (for non-expiring public URLs), `ServiceUrl` (custom endpoint for MinIO etc.), `AccessKey` / `SecretKey` (leave empty to use the AWS SDK credential chain), `ForcePathStyle` (only applies when `ServiceUrl` is set). +- **`S3StorageOptions`** - `Bucket`, `Region`, `Prefix`, `PublicRead` (default true), `PublicBaseUrl` (for non-expiring public URLs), `ServiceUrl` (custom endpoint for RustFS, MinIO, etc.), `AccessKey` / `SecretKey` (leave empty to use the AWS SDK credential chain), `ForcePathStyle` (only applies when `ServiceUrl` is set). ## How modules consume Storage @@ -108,10 +108,10 @@ The generic `T` names the owning type - local storage uses it as the folder segm "Provider": "s3", // or "local" "S3": { "Bucket": "fsh-uploads", - "ServiceUrl": "http://minio:9000", // omit for AWS S3 default + "ServiceUrl": "http://rustfs:9000", // omit for AWS S3 default "Region": "us-east-1", - "ForcePathStyle": true, // required for MinIO - "AccessKey": "minioadmin", + "ForcePathStyle": true, // required for RustFS / MinIO + "AccessKey": "rustfsadmin", "SecretKey": "set-via-secrets", "PublicBaseUrl": "https://cdn.example.com" // for non-expiring public URLs } @@ -147,7 +147,7 @@ The Files module ships an `IFileScanner` hook. If you're using `IStorageService` ## Gotchas -- **MinIO needs `ForcePathStyle = true`.** Virtual-hosted-style addressing puts the bucket name in the subdomain, which MinIO can't service without DNS gymnastics. The option defaults to `false` and only takes effect when `ServiceUrl` is set - set it explicitly for MinIO and other self-hosted S3-compatible services. +- **Self-hosted S3 stores (RustFS, MinIO) need `ForcePathStyle = true`.** Virtual-hosted-style addressing puts the bucket name in the subdomain, which they can't service without DNS gymnastics. The option defaults to `false` and only takes effect when `ServiceUrl` is set - set it explicitly for RustFS, MinIO, and other self-hosted S3-compatible services. - **Presigned URLs have a TTL.** Once it expires, the URL is dead. Use `BuildPublicUrl` for non-expiring public URLs (and a bucket policy that grants public-read on that prefix). - **`QuotaMeteredStorageService` is scoped.** It depends on `IQuotaService` which is scoped per request. Don't resolve it from a singleton or a hosted service without creating a scope. - **Local storage paths are not tenant-segmented.** Files land under `wwwroot/uploads/{owner-type}/`, and anything `BuildPublicUrl` points at is served statically without policy enforcement. Don't use `LocalStorageService` in multi-tenant production - it exists for dev and tests. diff --git a/src/content/docs/changelog/index.mdx b/src/content/docs/changelog/index.mdx index bed3fe42..199b6139 100644 --- a/src/content/docs/changelog/index.mdx +++ b/src/content/docs/changelog/index.mdx @@ -1,6 +1,6 @@ --- title: Overview -lastUpdated: 2026-08-07 +lastUpdated: 2026-09-25 description: Release notes and version history for fullstackhero. sidebar: order: 1 @@ -11,6 +11,13 @@ seo: Notable changes to the kit, newest first. +## 2026-09-25 + +- **Object storage: MinIO replaced with RustFS for local dev, Docker Compose, and integration tests (breaking for docker-compose).** The `minio/minio` and `minio/mc` images were removed from Docker Hub and `quay.io/minio` now refuses anonymous pulls, so fresh clones could no longer bring up the stack. The kit now ships [RustFS](https://rustfs.com) (`rustfs/rustfs:1.0.0`, S3-compatible, Apache-2.0) on the same ports - **9000** (S3 API) and **9001** (web console) - with bucket bootstrap done by a pinned `amazon/aws-cli:2.37.3` init container. Nothing changes in application code: the API still talks to it through the `s3` storage provider with `ForcePathStyle`, and production can keep pointing at AWS S3 or any other S3-compatible store. See PR [#1390](https://github.com/fullstackhero/dotnet-starter-kit/pull/1390). + - **Aspire:** the `minio` / `minio-init` resources are now `rustfs` / `rustfs-init`, and the AppHost parameters are renamed `minio-user` / `minio-password` → `rustfs-user` / `rustfs-password` (default `rustfsadmin`). If you set the old parameters in user secrets or config, rename them. The data volume is now `{appPrefix}-rustfs-data`, so local uploads start empty; delete the old `*-minio-data` volume when you no longer need it. + - **Breaking (docker-compose):** in `deploy/docker/.env`, rename `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` → `RUSTFS_ACCESS_KEY` / `RUSTFS_SECRET_KEY` (compose refuses to start without them). The services are now `rustfs` / `rustfs-init` and the volume `minio_data` → `rustfs_data`, so existing objects are **not** carried over: before upgrading, copy them out of the old MinIO bucket and into the new `fsh` bucket with `aws s3 sync` (against each `--endpoint-url`), or have users re-upload. `fsh new` now generates `RUSTFS_ACCESS_KEY` / `RUSTFS_SECRET_KEY` for new projects. + - **Integration tests:** `FshWebApplicationFactory` runs RustFS through the generic Testcontainers `ContainerBuilder` (the `Testcontainers.Minio` package is gone; the base `Testcontainers` package is added), waiting on `/health`. Its public members are renamed `Minio*` → `S3AccessKey` / `S3SecretKey` / `S3Bucket` / `S3ServiceUrl`; update any custom tests that used `MinioServiceUrl`. + ## 2026-08-07 The transactional outbox was rebuilt so that any module can publish, every tenant's events actually get dispatched, and the kit is safe to scale past one API instance. diff --git a/src/content/docs/compare/fsh-vs-blazorplate.mdx b/src/content/docs/compare/fsh-vs-blazorplate.mdx index c59435d8..a34b3fef 100644 --- a/src/content/docs/compare/fsh-vs-blazorplate.mdx +++ b/src/content/docs/compare/fsh-vs-blazorplate.mdx @@ -1,6 +1,6 @@ --- title: fullstackhero vs BlazorPlate -lastUpdated: 2026-05-19 +lastUpdated: 2026-09-25 description: How fullstackhero compares to BlazorPlate - the difference between a free, open-source .NET 10 starter kit and a paid closed-source SaaS template. sidebar: label: vs BlazorPlate @@ -32,7 +32,7 @@ BlazorPlate is a paid commercial multi-tenant SaaS starter for .NET, sold as a o | Background jobs | Hangfire 1.8 | Hangfire | | Observability | Serilog 4 + OpenTelemetry 1.15 (OTLP) | Serilog | | Realtime | SignalR (Valkey backplane) + Server-Sent Events | SignalR | -| File storage | Tenant-scoped MinIO / S3 abstraction | File storage primitives | +| File storage | Tenant-scoped S3 abstraction (RustFS locally) | File storage primitives | | Email | MailKit / SendGrid | Email service | | Webhooks | Tenant-scoped subscriptions + HMAC-signed payloads | Not included | | API browser | Scalar (OpenAPI 3.1) | Swagger | diff --git a/src/content/docs/cross-cutting-concerns/health-checks.mdx b/src/content/docs/cross-cutting-concerns/health-checks.mdx index 4a2d98dd..f1a7932a 100644 --- a/src/content/docs/cross-cutting-concerns/health-checks.mdx +++ b/src/content/docs/cross-cutting-concerns/health-checks.mdx @@ -1,6 +1,6 @@ --- title: Health checks -lastUpdated: 2026-06-11 +lastUpdated: 2026-09-25 description: Liveness + readiness endpoints - per-module database checks, Valkey, Hangfire, tenant migrations - for Kubernetes / Docker Compose / load balancer probes. sidebar: label: Health checks @@ -28,7 +28,7 @@ Liveness asks "is the process alive?" - restart if it fails. Readiness asks "is | `hangfire` | Kit's `HangfireHealthCheck` | When `EnableJobs = true` | | `db:tenants-migrations` | Kit's `TenantMigrationsHealthCheck` (Multitenancy module) | Always (with the Multitenancy module) | -There is no storage/MinIO health check - blob storage failures surface through the Files module's own error handling, not readiness. +There is no storage/S3 health check - blob storage failures surface through the Files module's own error handling, not readiness. ## How to wire probes diff --git a/src/content/docs/deployment/aspire.mdx b/src/content/docs/deployment/aspire.mdx index 31f4ef13..4b1cdcad 100644 --- a/src/content/docs/deployment/aspire.mdx +++ b/src/content/docs/deployment/aspire.mdx @@ -1,15 +1,15 @@ --- title: Local Orchestration with .NET Aspire -lastUpdated: 2026-06-11 -description: What spins up when you run the AppHost, and the decisions behind the local topology - Postgres, Valkey, MinIO, the migrator, the API, and both React apps. +lastUpdated: 2026-09-25 +description: What spins up when you run the AppHost, and the decisions behind the local topology - Postgres, Valkey, RustFS, the migrator, the API, and both React apps. sidebar: label: Local Orchestration (Aspire) order: 2 pageType: concept seo: title: '.NET Aspire local orchestration - services, ports, and' - description: 'How fullstackhero uses .NET Aspire to run the full stack locally with one command: Postgres + pgAdmin, Redis, MinIO, a one-shot DB migrator, the API, and…' - keywords: '.NET Aspire AppHost, Aspire Postgres Redis MinIO, dotnet aspire orchestration, fullstackhero local development' + description: 'How fullstackhero uses .NET Aspire to run the full stack locally with one command: Postgres + pgAdmin, Redis, RustFS, a one-shot DB migrator, the API, and…' + keywords: '.NET Aspire AppHost, Aspire Postgres Redis RustFS, dotnet aspire orchestration, fullstackhero local development' --- One command brings up the entire stack - databases, cache, object storage, the @@ -31,8 +31,8 @@ documents what it starts and **why** it's wired the way it is. | pgAdmin | *(sidecar)* | Web UI on **:5050**, auto-discovers every database on the server | Persistent | | Valkey | `redis` | Distributed cache (HybridCache L2) + SignalR backplane + Data Protection key ring (**Valkey** - a Redis-compatible, BSD-licensed Redis fork; resource name stays `redis`) | Persistent (data volume) | | RedisInsight | `redis-insight` | Key browser on **:5540**, pre-connected to the Valkey instance for inspecting cache keys in dev | Persistent | -| MinIO | `minio` | S3-compatible object storage, **:9000** (API) / **:9001** (console) | Persistent (data volume) | -| MinIO init | `minio-init` | One-shot: creates the `fsh-uploads` bucket + download policy, then exits | Run-once | +| RustFS | `rustfs` | S3-compatible object storage (`rustfs/rustfs:1.0.0`), **:9000** (S3 API) / **:9001** (web console) | Persistent (data volume) | +| RustFS init | `rustfs-init` | One-shot (`amazon/aws-cli`): creates the `fsh-uploads` bucket + a public-read `GetObject` policy, then exits | Run-once | | DB migrator | `fsh-starter-db-migrator` | One-shot: applies migrations across the tenant catalog + every tenant's module DBs (`apply --seed`, so the root admin exists), then exits | Run-once | | Demo seeder | `fsh-starter-demo-seeder` | One-shot, **dev-only**: runs `seed-demo` after the migrator to provision the `acme`/`globex` demo tenants + users, then exits | Run-once | | API | `fsh-starter-api` | The ASP.NET Core API (`net10.0`) | Long-running | @@ -44,7 +44,7 @@ The `fsh-starter-*` resource names - and the Docker volume names - are derived from the AppHost's assembly name. A CLI-scaffolded app (say `Acme.Store`) gets `acme-store-api`, `acme-store-admin`, and so on, so two FSH-based apps on one machine never collide on container or volume names. The third-party infra -(`postgres`, `redis`, `minio`) and the `fsh-db` database keep stable names. +(`postgres`, `redis`, `rustfs`) and the `fsh-db` database keep stable names. The Aspire dashboard opens automatically and shows every resource's state, @@ -52,7 +52,7 @@ logs, traces, and endpoints. The API's Scalar UI is at `/scalar`. The API waits for Postgres and Valkey to be healthy **and** for the one-shot -jobs (`minio-init`, `fsh-starter-db-migrator`, `fsh-starter-demo-seeder`) to finish before it starts. So the API +jobs (`rustfs-init`, `fsh-starter-db-migrator`, `fsh-starter-demo-seeder`) to finish before it starts. So the API never boots against an unmigrated database or a missing bucket - no retry loops, no first-request 500s. @@ -61,19 +61,19 @@ no first-request 500s. ### Persistent infra, run-once jobs -Postgres, Valkey, and MinIO use `ContainerLifetime.Persistent` with named data +Postgres, Valkey, and RustFS use `ContainerLifetime.Persistent` with named data volumes. Your data, pgAdmin layout, and uploaded files survive `dotnet run` -restarts, so the inner loop stays fast. The migrator and MinIO bootstrap are +restarts, so the inner loop stays fast. The migrator and RustFS bootstrap are **run-once** - they do their job and exit rather than linger as "unhealthy." -### MinIO is S3, so dev matches prod +### RustFS is S3, so dev matches prod Object storage uses the same `Storage__Provider = "s3"` code path locally as in production - only `ServiceUrl` and `ForcePathStyle` differ. The Files module's presigned-URL upload flow (browser → storage directly, bytes never proxied -through the API) is exercised end-to-end in dev. MinIO is configured to accept +through the API) is exercised end-to-end in dev. RustFS is configured to accept browser PUTs from the admin (`:5173`) and dashboard (`:5174`) origins via -`MINIO_API_CORS_ALLOW_ORIGIN`. +`RUSTFS_CORS_ALLOWED_ORIGINS`. The root credentials come from the `rustfs-user` / `rustfs-password` AppHost parameters (default `rustfsadmin`). ### The migrator is the production deploy step too @@ -123,7 +123,7 @@ not http. The API uses `UseHttpsRedirection()`, so a call to the http endpoint cross-origin redirects (a different scheme/port is cross-origin per the Fetch spec). Hitting https directly preserves the bearer token on every request. The Vite apps run un-proxied on fixed ports (5173 / 5174) so HMR works and the -origins line up with the MinIO CORS allow-list. +origins line up with the RustFS CORS allow-list. ### The React apps are optional @@ -142,10 +142,10 @@ and the AppHost omits both React apps entirely. | pgAdmin | 5050 | | Valkey | 6379 (container) | | RedisInsight | 5540 | -| MinIO | 9000 (API) · 9001 (console) | +| RustFS | 9000 (S3 API) · 9001 (console) | ## From local to cloud The local topology maps almost one-to-one onto AWS: Postgres → RDS, Valkey → -ElastiCache, MinIO → S3, the API container → ECS Fargate, and the two React apps +ElastiCache, RustFS → S3, the API container → ECS Fargate, and the two React apps → S3 + CloudFront. See [Deploy to AWS with Terraform](/docs/deployment/aws-terraform/). diff --git a/src/content/docs/frontend/dashboard.mdx b/src/content/docs/frontend/dashboard.mdx index 4c80616e..463309f3 100644 --- a/src/content/docs/frontend/dashboard.mdx +++ b/src/content/docs/frontend/dashboard.mdx @@ -1,6 +1,6 @@ --- title: Tenant dashboard -lastUpdated: 2026-07-13 +lastUpdated: 2026-09-25 description: The end-user-facing React + Vite app at clients/dashboard - catalog, chat, files, tickets, invoices, identity admin, plus real-time SignalR + SSE feeds. sidebar: label: Tenant dashboard @@ -125,7 +125,7 @@ A product management surface over the Catalog module: paginated product list wit `clients/dashboard/src/pages/files/` (with the orchestration in `src/hooks/use-file-upload.ts`) implements the kit's three-step presigned upload: 1. Browser calls `POST /api/v1/files/upload-url` with metadata (file name, size, content type, category) - the server mints a presigned PUT URL and reserves a `FileAsset` row. -2. Browser uploads bytes directly to MinIO / S3 via the presigned URL - no proxy through the API - with real upload progress. +2. Browser uploads bytes directly to S3 (RustFS locally) via the presigned URL - no proxy through the API - with real upload progress. 3. Browser calls `POST /api/v1/files/{id}/finalize` - the server verifies the object and flips the file from `PendingUpload` to `Available`. -`dotnet run --project src/Host/FSH.Starter.AppHost` brings up Postgres, Valkey, MinIO, the API, the admin, and the dashboard - all at once, with service discovery handling URLs. You don't need to run `npm run dev` separately unless you're isolating frontend work. +`dotnet run --project src/Host/FSH.Starter.AppHost` brings up Postgres, Valkey, RustFS, the API, the admin, and the dashboard - all at once, with service discovery handling URLs. You don't need to run `npm run dev` separately unless you're isolating frontend work. ## Running locally @@ -31,7 +31,7 @@ This starts everything: - API at https://localhost:7030 (http on 5030) - Admin at http://localhost:5173 - Dashboard at http://localhost:5174 -- Postgres at tcp/5432, Valkey at tcp/6379, MinIO at :9000 +- Postgres at tcp/5432, Valkey at tcp/6379, RustFS (S3) at :9000 Vite HMR works through Aspire - edits to frontend files reload instantly in the browser. diff --git a/src/content/docs/getting-started/install.mdx b/src/content/docs/getting-started/install.mdx index 26640b4e..0e1cb0e4 100644 --- a/src/content/docs/getting-started/install.mdx +++ b/src/content/docs/getting-started/install.mdx @@ -1,6 +1,6 @@ --- title: Install -lastUpdated: 2026-06-20 +lastUpdated: 2026-09-25 description: Every way to get the kit - the fsh CLI, the dotnet new template, a git clone, and GitHub (Use this template / Codespaces). sidebar: order: 4 @@ -115,7 +115,7 @@ However you installed, the AppHost launches the Aspire dashboard. Every service ## Next diff --git a/src/content/docs/getting-started/introduction.mdx b/src/content/docs/getting-started/introduction.mdx index 4835d736..a6141164 100644 --- a/src/content/docs/getting-started/introduction.mdx +++ b/src/content/docs/getting-started/introduction.mdx @@ -1,6 +1,6 @@ --- title: Introduction -lastUpdated: 2026-06-11 +lastUpdated: 2026-09-25 description: What fullstackhero is, what it ships with, and how it's put together. sidebar: label: Introduction @@ -29,7 +29,7 @@ It's aimed at teams that want to ship features on day one without spending a spr - **Quotas.** Per-tenant request and resource limits. - **Observability baked in.** OpenTelemetry traces, metrics, and logs (OTLP); Serilog structured logging; health checks; security/exception auditing. - **API docs.** OpenAPI + Scalar UI. Versioned routes via `Asp.Versioning` (`/api/v{version}/{module}`). -- **Cloud-ready.** `.NET Aspire` AppHost brings up Postgres, Valkey (a Redis-compatible, BSD-licensed Redis fork), MinIO, the migrator, the API, and both React apps locally with one command. Production deploy via Docker Compose included. +- **Cloud-ready.** `.NET Aspire` AppHost brings up Postgres, Valkey (a Redis-compatible, BSD-licensed Redis fork), RustFS (S3-compatible object storage), the migrator, the API, and both React apps locally with one command. Production deploy via Docker Compose included. - **Two frontends.** React + Vite admin console and dashboard, both in `clients/`. - **CLI + template.** Scaffold a fresh, fully renamed project in one command with the `fsh` CLI (`FullStackHero.CLI`) or the `dotnet new fsh` template (`FullStackHero.NET.StarterKit`) - both on NuGet as stable 10.0.0. Or clone the repo / use the GitHub template. @@ -148,7 +148,7 @@ The API never mutates data on startup. Demo data (acme/globex tenants, sample ca | Tracing / metrics | OpenTelemetry 1.15 (OTLP exporter) | | API docs | OpenAPI 10 + Scalar UI | | Errors | `ProblemDetails` (RFC 9457) via global exception handler | -| Orchestration | .NET Aspire 13.4 (Postgres + Valkey + MinIO + API + both React apps) | +| Orchestration | .NET Aspire 13.4 (Postgres + Valkey + RustFS + API + both React apps) | ### Frontends @@ -162,7 +162,7 @@ The API never mutates data on startup. Demo data (acme/globex tenants, sample ca | Layer | Tools | |------------------|------------------------------------------------------------------| | Unit | xUnit, Shouldly, NSubstitute, AutoFixture | -| Integration | `Microsoft.AspNetCore.Mvc.Testing`, Testcontainers (Postgres / Valkey / Minio) | +| Integration | `Microsoft.AspNetCore.Mvc.Testing`, Testcontainers (Postgres / Valkey / RustFS) | | Architecture | NetArchTest - enforces module boundaries | ### Distribution diff --git a/src/content/docs/getting-started/prerequisites.mdx b/src/content/docs/getting-started/prerequisites.mdx index 75184467..2aaecb0b 100644 --- a/src/content/docs/getting-started/prerequisites.mdx +++ b/src/content/docs/getting-started/prerequisites.mdx @@ -1,13 +1,13 @@ --- title: Prerequisites -lastUpdated: 2026-06-11 +lastUpdated: 2026-09-25 description: What you need installed locally before running a fullstackhero project. sidebar: order: 2 pageType: guide seo: title: 'Prerequisites - .NET 10 SDK, Docker, Node setup' - description: 'Tools you need before running fullstackhero locally: .NET 10 SDK, Docker (for Postgres, Valkey, and MinIO containers), and optional Node 20+ for the React frontends.' + description: 'Tools you need before running fullstackhero locally: .NET 10 SDK, Docker (for Postgres, Valkey, and RustFS containers), and optional Node 20+ for the React frontends.' keywords: '.NET 10 SDK install, fullstackhero prerequisites, .NET development environment' --- @@ -16,7 +16,7 @@ A short checklist. The rest of Getting Started assumes these are in place. ## Required - **.NET 10 SDK** - from [dotnet.microsoft.com/download](https://dotnet.microsoft.com/download). The repo's `global.json` pins SDK `10.0.100` (rolling forward to the latest feature band). -- **Docker** - used for the Postgres, Valkey, and MinIO containers (Aspire and the integration tests rely on it). Docker Desktop on macOS / Windows, Docker Engine on Linux. +- **Docker** - used for the Postgres, Valkey, and RustFS containers (Aspire and the integration tests rely on it). Docker Desktop on macOS / Windows, Docker Engine on Linux. - **Git** - for cloning the repo. No separate Aspire install is needed: the AppHost project pulls the Aspire SDK (`Aspire.AppHost.Sdk`) from NuGet during restore. diff --git a/src/content/docs/getting-started/quick-start.mdx b/src/content/docs/getting-started/quick-start.mdx index cc1b09b1..74fda599 100644 --- a/src/content/docs/getting-started/quick-start.mdx +++ b/src/content/docs/getting-started/quick-start.mdx @@ -1,13 +1,13 @@ --- title: Quick Start -lastUpdated: 2026-06-20 +lastUpdated: 2026-09-25 description: Clone fullstackhero and run the whole stack locally with one command. sidebar: order: 3 pageType: guide seo: title: 'Quick Start - clone and run the .NET 10 starter kit' - description: 'Clone the fullstackhero repo and run the full stack locally via .NET Aspire: Postgres, Valkey, MinIO, migrations, the API, and both React apps with one command.' + description: 'Clone the fullstackhero repo and run the full stack locally via .NET Aspire: Postgres, Valkey, RustFS, migrations, the API, and both React apps with one command.' keywords: 'fullstackhero quick start, clone .NET 10 starter kit, .NET Aspire local development, fsh CLI' --- @@ -47,7 +47,7 @@ Skip this if you only want the API. dotnet run --project src/Host/FSH.Starter.AppHost ``` -Aspire brings up Postgres (with pgAdmin), Valkey, and MinIO in containers, runs the DbMigrator (migrations + seed) and the demo seeder, then starts the API and both React apps - all wired together with connection strings and OTLP export. Watch everything come up in the Aspire dashboard. +Aspire brings up Postgres (with pgAdmin), Valkey, and RustFS in containers, runs the DbMigrator (migrations + seed) and the demo seeder, then starts the API and both React apps - all wired together with connection strings and OTLP export. Watch everything come up in the Aspire dashboard. Once it's green: diff --git a/src/content/docs/getting-started/troubleshooting.mdx b/src/content/docs/getting-started/troubleshooting.mdx index ff38bfcd..b7081722 100644 --- a/src/content/docs/getting-started/troubleshooting.mdx +++ b/src/content/docs/getting-started/troubleshooting.mdx @@ -1,6 +1,6 @@ --- title: Troubleshooting -lastUpdated: 2026-06-11 +lastUpdated: 2026-09-25 description: Symptom → cause → fix for the failure modes you actually hit on first run and in the daily dev loop. sidebar: order: 6 @@ -16,7 +16,7 @@ form. Resource and volume names come straight from `src/Host/FSH.Starter.AppHost/AppHost.cs` - the AppHost derives a per-app prefix from its assembly name, so for a stock clone the prefix is **`fsh-starter`** and the Docker volumes are `fsh-starter-postgres-data`, `fsh-starter-redis-data`, and -`fsh-starter-minio-data`. If you renamed the solution, swap the prefix accordingly. +`fsh-starter-rustfs-data`. If you renamed the solution, swap the prefix accordingly. ## First run @@ -51,7 +51,7 @@ locally installed Postgres on 5432 won't clash. | 5174 | Dashboard app (Vite) | | 5050 | pgAdmin | | 5540 | RedisInsight | -| 9000 / 9001 | MinIO API / console | +| 9000 / 9001 | RustFS S3 API / console | | 17273 / 15036 | Aspire dashboard https / http | | 4317 | Aspire OTLP ingestion | @@ -70,7 +70,7 @@ kill ## Containers & Aspire The infrastructure containers (`postgres`, its pgAdmin sidecar, `redis` (Valkey), -`redis-insight`, `minio`) all use `ContainerLifetime.Persistent` - they survive +`redis-insight`, `rustfs`) all use `ContainerLifetime.Persistent` - they survive AppHost shutdown on purpose. That's what makes restarts fast, and it's also the root of the next three entries. @@ -107,7 +107,7 @@ removing the container is safe. ```bash docker ps -a -docker rm -f # e.g. the redis / redis-insight / postgres / minio container +docker rm -f # e.g. the redis / redis-insight / postgres / rustfs container ``` Relaunch the AppHost; it recreates the containers on a fresh network and reattaches @@ -230,7 +230,7 @@ HangfireOptions__Password= **Symptom:** every test in `Integration.Tests` fails fast with `DockerUnavailableException`. -The integration suite runs the real API over Testcontainers (PostgreSQL + MinIO +The integration suite runs the real API over Testcontainers (PostgreSQL + RustFS spun up per test run), so a running Docker daemon is a hard requirement. This is environmental, not a regression. diff --git a/src/content/docs/modules/catalog.mdx b/src/content/docs/modules/catalog.mdx index 80481ac3..25f9cc93 100644 --- a/src/content/docs/modules/catalog.mdx +++ b/src/content/docs/modules/catalog.mdx @@ -1,6 +1,6 @@ --- title: Catalog module -lastUpdated: 2026-06-11 +lastUpdated: 2026-09-25 description: Production-grade product catalogue - brands, hierarchical categories, products with multi-image support, price + stock domain events, and soft delete. sidebar: label: Catalog @@ -219,7 +219,7 @@ Then any other module can subscribe via `IIntegrationEventHandler If your module owns files, ship a `FileAccessPolicy : IFileAccessPolicy` next to the rest of its code. Register it during your module's `ConfigureServices`. The Files module never knows about your domain - your domain knows what's allowed. @@ -20,7 +20,7 @@ If your module owns files, ship a `FileAccessPolicy : IFileAccessPolicy` next to ## What ships in v10 -- **Presigned upload flow** - `POST /files/upload-url` mints a presigned PUT URL with a 15-minute TTL (configurable). Client uploads directly to MinIO / S3. `POST /files/{id}/finalize` flips the file from `PendingUpload` to `Available`. +- **Presigned upload flow** - `POST /files/upload-url` mints a presigned PUT URL with a 15-minute TTL (configurable). Client uploads directly to S3 (RustFS locally). `POST /files/{id}/finalize` flips the file from `PendingUpload` to `Available`. - **Pluggable access policies** - `IFileAccessPolicy` interface registered per `OwnerType`. Catalog ships `ProductFileAccessPolicy`, Chat ships `ChatChannelFileAccessPolicy`, the Files module itself registers `DefaultUploaderOnlyPolicy` for the built-in `MyFiles` and `User` owner types. - **Per-category validation** - extension whitelist + size cap per category (`Image` 10 MB, `Document` 25 MB, `Archive` 50 MB), configured in `appsettings`. - **Visibility** - files are `Public` or `Private`; `PATCH /files/{id}/visibility` flips it after upload (policy-gated, only on `Available` files). `GET /files/shared` lists the tenant's public free-standing files (`MyFiles` / `User` owner types) for a "Shared in tenant" surface. @@ -188,7 +188,7 @@ Endpoints marked "- (policy)" require authentication only; the per-OwnerType `IF The section binds to `FilesOptions` (`Modules.Files/FilesOptions.cs`); category names are matched case-insensitively. -The S3 / MinIO target is configured in the [Storage building block](/docs/building-blocks/storage/) - the Files module is the policy + lifecycle layer on top. +The S3 target is configured in the [Storage building block](/docs/building-blocks/storage/) - the Files module is the policy + lifecycle layer on top. ## How to extend @@ -242,10 +242,10 @@ No module ships a consumer today - Catalog and Chat attach files by passing the - `DefaultUploaderOnlyPolicyTests` - policy enforcement - `FileAccessPolicyRegistryTests` - registry lookup - `StorageKeyBuilderTests` - tenant + category path generation -- Integration tests at `src/Tests/Integration.Tests/Tests/Files/` (ten files) cover the presigned round-trip (`RequestAndFinalizeUploadTests`, `StorageFlowTests`), upload validation, finalize edge cases, visibility + sharing, soft delete + restore, the purge jobs, and tenant isolation against Testcontainers MinIO. +- Integration tests at `src/Tests/Integration.Tests/Tests/Files/` (ten files) cover the presigned round-trip (`RequestAndFinalizeUploadTests`, `StorageFlowTests`), upload validation, finalize edge cases, visibility + sharing, soft delete + restore, the purge jobs, and tenant isolation against Testcontainers RustFS. ## Related -- [Storage building block](/docs/building-blocks/storage/) - the S3 / MinIO abstraction this module sits on top of. +- [Storage building block](/docs/building-blocks/storage/) - the S3-compatible abstraction this module sits on top of. - [Catalog module](/docs/modules/catalog/) - product images use the policy hook. - [Chat module](/docs/modules/chat/) - channel attachments use the policy hook. diff --git a/src/content/docs/security/production-checklist.mdx b/src/content/docs/security/production-checklist.mdx index d0cedb32..1eb110ff 100644 --- a/src/content/docs/security/production-checklist.mdx +++ b/src/content/docs/security/production-checklist.mdx @@ -1,6 +1,6 @@ --- title: Production security checklist -lastUpdated: 2026-06-11 +lastUpdated: 2026-09-25 description: Ten configuration items you must check before shipping fullstackhero to production. Skip none. sidebar: label: Production checklist @@ -181,7 +181,7 @@ These aren't blockers, but the sooner the better: - **Make 2FA mandatory for admin roles.** A leaked admin password compromises everything; require TOTP. - **Add monitoring + alerts** on auth-failure spikes, impersonation events, and 5xx error rates. - **Run a vulnerability scan** of the deployed image (Snyk, Trivy, Dependabot) to catch transitive CVEs early. -- **Set up backup + restore tests** for Postgres + MinIO. Backups you haven't tested restoring are not backups. +- **Set up backup + restore tests** for Postgres + RustFS. Backups you haven't tested restoring are not backups. - **Document an incident-response runbook** - who's on-call, where the alerts go, how to revoke every session (admin revoke-all is built in), how to rotate the JWT signing key in an emergency. ## Related diff --git a/src/content/docs/testing/fixtures-and-seed-data.mdx b/src/content/docs/testing/fixtures-and-seed-data.mdx index 0c55cc0f..de5a7d6f 100644 --- a/src/content/docs/testing/fixtures-and-seed-data.mdx +++ b/src/content/docs/testing/fixtures-and-seed-data.mdx @@ -1,6 +1,6 @@ --- title: Fixtures & seed data -lastUpdated: 2026-06-11 +lastUpdated: 2026-09-25 description: The shared test factory (containers, host, seeded root tenant + admin) used across the integration suite, and the service swaps that keep tests deterministic. sidebar: label: Fixtures & seed data @@ -22,7 +22,7 @@ The whole suite shares one database across hundreds of tests. Give everything yo | Piece | Type | Role | |---|---|---| -| `FshWebApplicationFactory` | `WebApplicationFactory, IAsyncLifetime` | Owns the `postgres:17-alpine` + `minio/minio:latest` Testcontainers, builds the in-process host, migrates + seeds | +| `FshWebApplicationFactory` | `WebApplicationFactory, IAsyncLifetime` | Owns the `postgres:17-alpine` + `rustfs/rustfs:1.0.0` (generic `ContainerBuilder`) Testcontainers, builds the in-process host, migrates + seeds | | `FshCollectionDefinition` | `ICollectionFixture` | Shares **one** factory instance across the whole suite (`[Collection(FshCollectionDefinition.Name)]`) | | `AuthHelper` | Plain class, wraps the factory | Issues real tokens via `POST /token/issue` and builds authenticated `HttpClient`s | | `TestConstants` | Static class | Root tenant id, root admin credentials, JWT settings, per-module base paths | @@ -48,14 +48,14 @@ builder.ConfigureAppConfiguration((_, config) => ["RateLimitingOptions:Enabled"] = "false", ["SecurityHeadersOptions:Enabled"] = "false", ["Storage:Provider"] = "s3", - ["Storage:S3:ServiceUrl"] = _minio.GetConnectionString(), + ["Storage:S3:ServiceUrl"] = S3ServiceUrl, // ... bucket, keys, region }); }); ``` -`AddHeroStorage` reads `Storage:Provider` **eagerly**, before this overlay applies - so the factory also removes the registered `IStorageService` and re-registers the S3 stack against the MinIO container in `ConfigureServices` (`RewireStorageForS3`). Config-only overrides aren't enough for eagerly-bound services. +`AddHeroStorage` reads `Storage:Provider` **eagerly**, before this overlay applies - so the factory also removes the registered `IStorageService` and re-registers the S3 stack against the RustFS container in `ConfigureServices` (`RewireStorageForS3`). Config-only overrides aren't enough for eagerly-bound services. The factory also swaps Hangfire to `Hangfire.InMemory` (with a real server polling every second, so jobs actually run), replaces mail with `NoOpMailService`, and removes hosted services that would race the test migrations (role-permission sync, outbox dispatcher). @@ -128,7 +128,7 @@ Finbuckle's `IMultiTenantContextSetter` writes an `AsyncLocal`. Set it **in the ## When to add to the harness -- **New shared infrastructure.** If you need RabbitMQ, Elasticsearch, etc., add the container as another field on `FshWebApplicationFactory`, start it in `InitializeAsync`, and overlay its connection details - same shape as the MinIO wiring. For something only one test class needs (like the Valkey container in `HybridCacheRedisTests`), keep it local to that class via `IAsyncLifetime` instead. +- **New shared infrastructure.** If you need RabbitMQ, Elasticsearch, etc., add the container as another field on `FshWebApplicationFactory`, start it in `InitializeAsync`, and overlay its connection details - same shape as the RustFS wiring. For something only one test class needs (like the Valkey container in `HybridCacheRedisTests`), keep it local to that class via `IAsyncLifetime` instead. - **New always-present seed data.** Extend the module's `IDbInitializer` seed - that's the production path, and the factory runs it automatically. - **One-off setup per test class.** Don't touch the harness; arrange it in the test class constructor or the test itself. diff --git a/src/content/docs/testing/index.mdx b/src/content/docs/testing/index.mdx index a899ceba..12dfffa1 100644 --- a/src/content/docs/testing/index.mdx +++ b/src/content/docs/testing/index.mdx @@ -1,17 +1,17 @@ --- title: Overview -lastUpdated: 2026-06-11 +lastUpdated: 2026-09-25 description: Three layers of tests - unit (xUnit + Shouldly + NSubstitute + AutoFixture), integration (Testcontainers), and architecture (NetArchTest) - around 1,500 tests green on every build. sidebar: order: 1 pageType: guide seo: title: 'Testing in .NET 10 - xUnit, Testcontainers, NetArchTest in' - description: 'How fullstackhero is tested - three layers (unit, integration with Testcontainers Postgres+MinIO, architecture with NetArchTest), around 1,500 tests…' + description: 'How fullstackhero is tested - three layers (unit, integration with Testcontainers Postgres+RustFS, architecture with NetArchTest), around 1,500 tests…' keywords: '.net 10 testing, xunit shouldly nsubstitute, testcontainers .net postgres, netarchtest module boundaries, integration tests dotnet aspnet' --- -fullstackhero ships with around **1,500 `[Fact]` + `[Theory]` tests** that run green on every build. Three layers: unit tests verify domain invariants and handler logic without infrastructure; integration tests spin up real Postgres + MinIO via Testcontainers and exercise the API end-to-end through `Microsoft.AspNetCore.Mvc.Testing`; architecture tests enforce module boundaries, contracts purity, and naming conventions as compiler-level rules. +fullstackhero ships with around **1,500 `[Fact]` + `[Theory]` tests** that run green on every build. Three layers: unit tests verify domain invariants and handler logic without infrastructure; integration tests spin up real Postgres + RustFS via Testcontainers and exercise the API end-to-end through `Microsoft.AspNetCore.Mvc.Testing`; architecture tests enforce module boundaries, contracts purity, and naming conventions as compiler-level rules. Every structural rule in the kit - "modules never reference each other's runtime", "every command handler has a validator", "contracts assemblies stay dependency-pure" - is an assertion in `src/Tests/Architecture.Tests/`. Break the rule, fail the build. Behavioural guarantees like permission enforcement are covered end-to-end in the integration suite instead. diff --git a/src/content/docs/testing/integration-tests.mdx b/src/content/docs/testing/integration-tests.mdx index bf11e368..1645ecc2 100644 --- a/src/content/docs/testing/integration-tests.mdx +++ b/src/content/docs/testing/integration-tests.mdx @@ -1,18 +1,18 @@ --- title: Integration tests -lastUpdated: 2026-06-11 -description: WebApplicationFactory in-process API tests against Testcontainers Postgres + MinIO - one container set, all modules, real round trips. +lastUpdated: 2026-09-25 +description: WebApplicationFactory in-process API tests against Testcontainers Postgres + RustFS - one container set, all modules, real round trips. sidebar: label: Integration tests order: 2 pageType: guide seo: title: 'Integration tests in .NET 10 - WebApplicationFactory' - description: 'How fullstackhero runs integration tests against real Postgres + MinIO via Testcontainers, in-process API tests through WebApplicationFactory…' - keywords: 'webapplicationfactory dotnet, testcontainers postgres .net 10, integration test asp.net core, in-process api test, testcontainers minio' + description: 'How fullstackhero runs integration tests against real Postgres + RustFS via Testcontainers, in-process API tests through WebApplicationFactory…' + keywords: 'webapplicationfactory dotnet, testcontainers postgres .net 10, integration test asp.net core, in-process api test, testcontainers rustfs s3' --- -Integration tests in fullstackhero spin up real infrastructure in containers - PostgreSQL 17 and MinIO - and exercise the API end-to-end through `Microsoft.AspNetCore.Mvc.Testing`'s `WebApplicationFactory`. One factory (and therefore one container set) serves the entire `Integration.Tests` project via an xUnit collection fixture, so the startup cost is paid once and the per-test cost is small. +Integration tests in fullstackhero spin up real infrastructure in containers - PostgreSQL 17 and RustFS (S3-compatible) - and exercise the API end-to-end through `Microsoft.AspNetCore.Mvc.Testing`'s `WebApplicationFactory`. One factory (and therefore one container set) serves the entire `Integration.Tests` project via an xUnit collection fixture, so the startup cost is paid once and the per-test cost is small. Splitting integration tests into per-module projects would mean per-project container startup. The single project shares one factory + one container set across every test class, runs faster, and you don't have to think about container leakage across boundaries. @@ -23,7 +23,7 @@ Splitting integration tests into per-module projects would mean per-project cont | Layer | Library | Purpose | |---|---|---| | Host | `Microsoft.AspNetCore.Mvc.Testing` | `WebApplicationFactory` for the in-process API | -| Containers | `Testcontainers.PostgreSql` / `Testcontainers.Minio` 4.11 | `postgres:17-alpine` + `minio/minio:latest` per test run | +| Containers | `Testcontainers.PostgreSql` + generic `Testcontainers` (`ContainerBuilder`) 4.14 | `postgres:17-alpine` + `rustfs/rustfs:1.0.0` per test run | | HTTP | `HttpClient` from `factory.CreateClient()` | Real HTTP calls through the full middleware pipeline | | Assertions | Shouldly, NSubstitute, AutoFixture | Same as unit tests | @@ -31,7 +31,7 @@ There is **no Redis/Valkey container** in the shared harness - `CachingOptions:R ## The fixture pattern -`FshWebApplicationFactory` (in `src/Tests/Integration.Tests/Infrastructure/`) extends `WebApplicationFactory` and owns the Postgres + MinIO containers as fields. It is shared across the whole suite through a **collection fixture**: +`FshWebApplicationFactory` (in `src/Tests/Integration.Tests/Infrastructure/`) extends `WebApplicationFactory` and owns the Postgres + RustFS containers as fields. It is shared across the whole suite through a **collection fixture**: ```csharp // Infrastructure/FshCollectionDefinition.cs @@ -80,7 +80,7 @@ public sealed class BrandsEndpointTests } ``` -On startup (`IAsyncLifetime.InitializeAsync`) the factory starts both containers in parallel, creates the MinIO bucket, then **migrates the tenant catalog, seeds the root tenant, and runs every module's `IDbInitializer` migrate + seed** - including the production `RolePermissionSyncer`. A semaphore stops test classes from racing the migration. +On startup (`IAsyncLifetime.InitializeAsync`) the factory starts both containers in parallel, creates the S3 bucket, then **migrates the tenant catalog, seeds the root tenant, and runs every module's `IDbInitializer` migrate + seed** - including the production `RolePermissionSyncer`. A semaphore stops test classes from racing the migration. ## Authenticated test clients @@ -103,10 +103,10 @@ The factory overrides configuration and services so tests are deterministic: - **Mail** is a `NoOpMailService` - no SMTP, no retries. - **Exceptions** surface through a `DetailedTestExceptionHandler` instead of the generic production error response, so failures tell you what broke. - **Rate limiting and security headers** are disabled (they get their own dedicated suite - see below). -- **Storage** is rewired to S3-against-MinIO *after* registration: +- **Storage** is rewired to S3-against-RustFS *after* registration: -`AddHeroStorage` reads `Storage:Provider` from configuration **eagerly** - before the test config overlay applies - so it wires the local-disk provider. The factory removes the registered `IStorageService` and re-registers the S3 stack pointed at the MinIO container (`RewireStorageForS3`). If you build your own factory, you need the same trick. +`AddHeroStorage` reads `Storage:Provider` from configuration **eagerly** - before the test config overlay applies - so it wires the local-disk provider. The factory removes the registered `IStorageService` and re-registers the S3 stack pointed at the RustFS container (`RewireStorageForS3`). If you build your own factory, you need the same trick. ## Two more harness gotchas diff --git a/src/content/docs/testing/writing-new-tests.mdx b/src/content/docs/testing/writing-new-tests.mdx index da0d4ac0..e72a99fb 100644 --- a/src/content/docs/testing/writing-new-tests.mdx +++ b/src/content/docs/testing/writing-new-tests.mdx @@ -1,6 +1,6 @@ --- title: Writing new tests -lastUpdated: 2026-06-11 +lastUpdated: 2026-09-25 description: Step-by-step recipes for adding a unit test, an integration test, or an architecture rule - copy-paste-ready. sidebar: label: Writing new tests @@ -175,7 +175,7 @@ dotnet test src/Tests/Integration.Tests/ \ --filter "FullyQualifiedName~AdjustProductStockEndpointTests" ``` -Docker must be running - the suite starts its Postgres + MinIO containers once and shares them across every test. +Docker must be running - the suite starts its Postgres + RustFS containers once and shares them across every test. ## Recipe 3 - A new architecture rule