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