Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/rules/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,5 @@ No global mutable static collections enumerated under concurrency. `Audit` (Audi
- `appsettings.json` (+ `.Development`/`.Production`) live in `src/Host/FSH.Starter.Api/`. DbMigrator links the same files.
- Bind config with the Options pattern: `AddOptions<T>().BindConfiguration(nameof(T))`, section name == type name (e.g. `JwtOptions`, `DatabaseOptions`, `CachingOptions`, `CorsOptions`, `QuotaOptions`, `RateLimitingOptions`; **storage section is `Storage`**, not `StorageOptions`). Add `.ValidateDataAnnotations().ValidateOnStart()` for fail-fast.
- Validate critical options via `IValidatableObject` — `JwtOptions` requires `SigningKey` ≥32 chars and **rejects placeholder strings containing `"replace-with"`**; `DatabaseOptions` rejects empty connection strings.
- **Production fail-fast** (`Program.cs`, before service registration): missing `DatabaseOptions:ConnectionString`, `CachingOptions:Redis`, or `JwtOptions:SigningKey` throws. Dev secrets via `dotnet user-secrets` (AppHost has a `UserSecretsId`); MinIO creds are Aspire secret parameters.
- **Production fail-fast** (`Program.cs`, before service registration): missing `DatabaseOptions:ConnectionString`, `CachingOptions:Redis`, or `JwtOptions:SigningKey` throws. Dev secrets via `dotnet user-secrets` (AppHost has a `UserSecretsId`); RustFS (S3) creds are Aspire secret parameters.
- Platform composition is one call each: `builder.AddHeroPlatform(o => { o.Enable... })` (DI) and `app.UseHeroPlatform(...)` (middleware). Feature flags toggle Caching/Jobs/Mailing/Quotas/Sse/Realtime/OpenTelemetry/CORS/Idempotency.
6 changes: 3 additions & 3 deletions .agents/rules/integration-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

## Harness

`WebApplicationFactory` over **real** infra via Testcontainers — PostgreSQL + Redis + MinIO. **Docker must be running**; if it isn't, tests fail fast with `DockerUnavailableException` (environmental, not a regression — run the unit projects instead).
`WebApplicationFactory` over **real** infra via Testcontainers — PostgreSQL + Redis + RustFS (S3-compatible, generic container). **Docker must be running**; if it isn't, tests fail fast with `DockerUnavailableException` (environmental, not a regression — run the unit projects instead).

`FshWebApplicationFactory` (`Integration.Tests/Infrastructure/`) boots the containers, overlays in-memory config, swaps `IMailService` → `NoOpMailService`, and rewires storage to MinIO.
`FshWebApplicationFactory` (`Integration.Tests/Infrastructure/`) boots the containers, overlays in-memory config, swaps `IMailService` → `NoOpMailService`, and rewires storage to RustFS.

## Must-know gotchas

- **Tenant context is AsyncLocal — set it inline.** Set the Finbuckle tenant context **in the same method** as the `UserManager`/`DbContext` call. Setting it in an awaited helper loses it across the async boundary → NRE in the tenant query filter.
- **Storage is wired eagerly.** `AddHeroStorage` reads `Storage:Provider` before the test config overlay, so it picks `LocalStorageService`. The factory **removes the `IStorageService`/`LocalStorageService`/`S3StorageService` descriptors post-registration and re-registers the S3 stack** at MinIO. Follow that when a test needs real object storage. (See `storage.md`.)
- **Storage is wired eagerly.** `AddHeroStorage` reads `Storage:Provider` before the test config overlay, so it picks `LocalStorageService`. The factory **removes the `IStorageService`/`LocalStorageService`/`S3StorageService` descriptors post-registration and re-registers the S3 stack** at RustFS. Follow that when a test needs real object storage. (See `storage.md`.)
- **SignalR tests force long-polling** — TestServer has no WebSocket. Configure the client transport accordingly.
- **Rate limiting is read eagerly** — `Integration.Middleware.Tests` sets `RateLimitingOptions:Enabled` via env var **before** host build, since flipping it after has no effect.

Expand Down
6 changes: 3 additions & 3 deletions .agents/rules/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

## Providers

`AddHeroStorage(config)` reads `Storage:Provider` **eagerly at registration**: `"s3"` → `S3StorageService` (supports MinIO via `ServiceUrl` + `ForcePathStyle`), else `LocalStorageService`. When quotas are enabled the service is wrapped in `QuotaMeteredStorageService` (debits `StorageBytes`).
`AddHeroStorage(config)` reads `Storage:Provider` **eagerly at registration**: `"s3"` → `S3StorageService` (supports any S3-compatible store, e.g. RustFS, via `ServiceUrl` + `ForcePathStyle`), else `LocalStorageService`. When quotas are enabled the service is wrapped in `QuotaMeteredStorageService` (debits `StorageBytes`).

## Presigned upload flow (preferred for user uploads)

Expand All @@ -19,8 +19,8 @@ Don't stream large files through the API. The pattern (see Files module):
2. Client uploads **directly** to storage.
3. `FinalizeUpload` — flips to `Available`, **debits the quota here** (not at request time), publishes `FileFinalizedIntegrationEvent`.

Local/dev without MinIO uses `LocalPresignTokenStore` (in-memory one-shot tokens).
Local/dev without an S3 store uses `LocalPresignTokenStore` (in-memory one-shot tokens).

## Test gotcha

`AddHeroStorage` reads `Storage:Provider` **before** a test factory's in-memory config overlay applies, so it wires `LocalStorageService`. Integration tests that need MinIO must **remove the `IStorageService`/`LocalStorageService`/`S3StorageService` descriptors post-registration and re-register the S3 stack** pointed at the MinIO container (see `FshWebApplicationFactory`). See `integration-testing.md`.
`AddHeroStorage` reads `Storage:Provider` **before** a test factory's in-memory config overlay applies, so it wires `LocalStorageService`. Integration tests that need object storage must **remove the `IStorageService`/`LocalStorageService`/`S3StorageService` descriptors post-registration and re-register the S3 stack** pointed at the RustFS container (see `FshWebApplicationFactory`). See `integration-testing.md`.
2 changes: 1 addition & 1 deletion .agents/rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ xUnit · Shouldly (`result.ShouldBe(...)`) · NSubstitute (`Substitute.For<IServ
| `{Module}.Tests` | Unit: handlers, services, domain | no |
| `Framework.Tests`, `Generic.Tests`, `Caching.Tests` | BuildingBlocks units | no |
| `Architecture.Tests` | NetArchTest: module boundaries + tenant-isolation rules + handler↔validator pairing | no |
| `Integration.Tests` | `WebApplicationFactory` over real PostgreSQL/Redis/MinIO | **yes** |
| `Integration.Tests` | `WebApplicationFactory` over real PostgreSQL/Redis/RustFS | **yes** |
| `Integration.Middleware.Tests` | Real middleware wiring | **yes** |

```bash
Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/testing-guide/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ Don't weaken these to make a change pass — fix the code.

## Integration tests

`Integration.Tests` runs over real Postgres/Redis/MinIO via Testcontainers — **Docker required**. Set the
Finbuckle tenant context inline, rewire `IStorageService` post-registration for MinIO, force long-polling
`Integration.Tests` runs over real Postgres/Redis/RustFS via Testcontainers — **Docker required**. Set the
Finbuckle tenant context inline, rewire `IStorageService` post-registration for RustFS (S3), force long-polling
for SignalR. All detailed in `.agents/rules/integration-testing.md`.

## Run
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ front-ends and a CLI. Multitenancy, auth, auditing, billing, files, chat and mor
| `src/BuildingBlocks/` | Shared framework libraries (Core, Persistence, Web, Caching, Eventing, Storage, Quota…). **Protected — see below.** |
| `src/Modules/{Name}/` | Bounded contexts. Each has a runtime project + a `.Contracts` project (its only public API). |
| `src/Host/FSH.Starter.Api` | Composition-root Web API host. |
| `src/Host/FSH.Starter.AppHost` | .NET Aspire orchestrator (Postgres, Redis, MinIO, migrator, API, **both React apps**). |
| `src/Host/FSH.Starter.AppHost` | .NET Aspire orchestrator (Postgres, Redis, RustFS, migrator, API, **both React apps**). |
| `src/Host/FSH.Starter.DbMigrator` | One-shot migrate/seed runner. DB is **not** migrated at API startup. |
| `src/Host/FSH.Starter.Migrations.PostgreSQL` | All EF migrations, organized per-module by folder. |
| `src/Tests/` | Per-module tests, `Architecture.Tests` (NetArchTest), `Integration.Tests` (Testcontainers). |
Expand All @@ -51,7 +51,7 @@ front-ends and a CLI. Multitenancy, auth, auditing, billing, files, chat and mor
## Build & run

```bash
# Whole stack (Postgres + pgAdmin + Redis + MinIO + migrator + API + both React apps)
# Whole stack (Postgres + pgAdmin + Redis + RustFS + migrator + API + both React apps)
dotnet run --project src/Host/FSH.Starter.AppHost # one-time: npm install in clients/admin & clients/dashboard

dotnet build src/FSH.Starter.slnx # build backend
Expand All @@ -68,7 +68,7 @@ dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply [--seed]
dotnet run --project src/Host/FSH.Starter.DbMigrator -- list-pending
```

**Ports:** API 7030 (https)/5030 (http) · admin 5173 · dashboard 5174 · Postgres 5432 · pgAdmin 5050 · Valkey 6379 · MinIO 9000/9001.
**Ports:** API 7030 (https)/5030 (http) · admin 5173 · dashboard 5174 · Postgres 5432 · pgAdmin 5050 · Valkey 6379 · RustFS 9000/9001 (S3 API/console).

## Branching & PRs

Expand Down
4 changes: 2 additions & 2 deletions README-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ the shared code lives in `src/BuildingBlocks` and is yours to change.

- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- [Node.js 20+](https://nodejs.org) — for the React apps
- [Docker](https://www.docker.com/) — Postgres, Redis, MinIO (orchestrated by Aspire)
- [Docker](https://www.docker.com/) — Postgres, Redis, RustFS (orchestrated by Aspire)

## Quick start

Expand All @@ -21,7 +21,7 @@ the shared code lives in `src/BuildingBlocks` and is yours to change.
dotnet run --project src/Host/FSH.Starter.AppHost
```

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

| Surface | URL |
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ You scaffold with the `fsh` CLI and get the **complete, detached source** — ev
dotnet tool install -g FullStackHero.CLI
fsh new MyApp
cd MyApp
dotnet run --project src/Host/MyApp.AppHost # 🎉 whole stack up: API + 2 React apps + Postgres + Valkey + MinIO
dotnet run --project src/Host/MyApp.AppHost # 🎉 whole stack up: API + 2 React apps + Postgres + Valkey + RustFS
```

> Then open the **Aspire dashboard** at `https://localhost:15888`, the **API + Scalar docs** at `https://localhost:7030/scalar`, the **admin** app at `http://localhost:5173`, and the **dashboard** app at `http://localhost:5174`. Sign in with a seeded demo account (e.g. `admin@acme.com` / `Password123!`).
Expand All @@ -43,7 +43,7 @@ dotnet run --project src/Host/MyApp.AppHost # 🎉 whole stack up: API + 2 Rea
- **EF Core 10** on **PostgreSQL** (Npgsql), with domain events, the specification pattern, soft-delete + audit interceptors, and tenant-isolated `DbContext`s.
- **JWT auth + ASP.NET Identity** — issuance/refresh, roles & fine-grained permissions, rate-limited auth, password policies, sessions, impersonation.
- **Multitenancy** via [Finbuckle](https://www.finbuckle.com/) — tenant resolution, provisioning, per-tenant migrations & seeding, isolation enforced by default.
- **Cross-cutting**: HybridCache on **Valkey** (Redis-compatible), **Hangfire** jobs, presigned S3/**MinIO** storage, mailing, idempotency, quotas, rate limiting, API versioning, RFC 9457 `ProblemDetails`.
- **Cross-cutting**: HybridCache on **Valkey** (Redis-compatible), **Hangfire** jobs, presigned S3 storage (**RustFS** locally), mailing, idempotency, quotas, rate limiting, API versioning, RFC 9457 `ProblemDetails`.
- **Observability**: Serilog structured logging + **OpenTelemetry** traces/metrics/logs, health probes, security/exception auditing.
- **Docs**: **OpenAPI** + the **Scalar** API reference UI.

Expand All @@ -55,7 +55,7 @@ dotnet run --project src/Host/MyApp.AppHost # 🎉 whole stack up: API + 2 Rea
**Identity · Multitenancy · Billing · Catalog · Tickets · Chat · Files · Webhooks · Auditing · Notifications** — each a runtime project plus a `.Contracts` project (its only public surface), boundaries enforced by architecture tests.

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

Expand Down Expand Up @@ -96,7 +96,7 @@ git clone https://github.com/fullstackhero/dotnet-starter-kit.git MyApp && cd My
dotnet run --project src/Host/FSH.Starter.AppHost
```

> **Prerequisites:** [.NET 10 SDK](https://dotnet.microsoft.com/download) · [Docker](https://www.docker.com/) (Postgres/Valkey/MinIO via Aspire) · [Node 20+](https://nodejs.org/) (for the React apps).
> **Prerequisites:** [.NET 10 SDK](https://dotnet.microsoft.com/download) · [Docker](https://www.docker.com/) (Postgres/Valkey/RustFS via Aspire) · [Node 20+](https://nodejs.org/) (for the React apps).

**`fsh` commands:** `new` · `doctor` · `info` · `update` · `--version`. Full reference → [fullstackhero.net/docs/cli](https://fullstackhero.net/docs/cli/).

Expand All @@ -113,7 +113,7 @@ dotnet run --project src/Host/FSH.Starter.AppHost
| Auth | JWT + ASP.NET Identity | Realtime | SignalR · SSE |
| Multitenancy | Finbuckle 10 | Tests | Playwright |
| Cache / Jobs | Valkey · Hangfire | | |
| Storage | S3 / MinIO (presigned) | **Infra** | |
| Storage | S3 / RustFS (presigned) | **Infra** | |
| Docs | OpenAPI + Scalar | Orchestration | .NET Aspire |
| Observability | Serilog + OpenTelemetry | Deploy | Docker Compose · Terraform |
| Testing | xUnit · Testcontainers · NetArchTest | | |
Expand All @@ -127,7 +127,7 @@ dotnet run --project src/Host/FSH.Starter.AppHost
| `src/BuildingBlocks/` | Shared framework libraries (Core, Persistence, Web, Caching, Eventing, Storage, Quota…) |
| `src/Modules/{Name}/` | Bounded contexts — each with a runtime project + a `.Contracts` project (its public API) |
| `src/Host/FSH.Starter.Api` | Composition-root Web API host |
| `src/Host/FSH.Starter.AppHost` | .NET Aspire orchestrator (Postgres, Valkey, MinIO, migrator, API, both React apps) |
| `src/Host/FSH.Starter.AppHost` | .NET Aspire orchestrator (Postgres, Valkey, RustFS, migrator, API, both React apps) |
| `src/Host/FSH.Starter.DbMigrator` | One-shot migrate/seed runner (DB is **not** migrated at API startup) |
| `src/Tools/CLI` | The `fsh` CLI (Spectre.Console) |
| `clients/admin`, `clients/dashboard` | The two React apps |
Expand Down
2 changes: 1 addition & 1 deletion clients/admin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Two options — pick whichever matches how you want to develop.

### Option A — run everything through Aspire (recommended)

The AppHost launches Postgres, Redis, MinIO, the API, **and** this Vite app together, with `VITE_API_BASE_URL` wired via service discovery.
The AppHost launches Postgres, Redis, RustFS, the API, **and** this Vite app together, with `VITE_API_BASE_URL` wired via service discovery.

```bash
npm install --prefix clients/admin # one-time
Expand Down
2 changes: 1 addition & 1 deletion clients/admin/src/components/file/image-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".webp", ".gif"];

/**
* ImageInput — composite control that lets a user either upload a new image
* (presigned PUT to S3/MinIO) OR paste an external URL. After a successful
* (presigned PUT to S3) OR paste an external URL. After a successful
* upload the component fetches the FileAsset metadata to retrieve the durable
* `publicUrl` and forwards it through `onChange`.
*/
Expand Down
2 changes: 1 addition & 1 deletion clients/admin/src/hooks/use-file-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const DEFAULT_OPTIONS = {
/**
* Orchestrates the three-step presigned-upload protocol:
* 1. POST /api/v1/files/upload-url — server mints presigned PUT + reserves a FileAsset row
* 2. PUT <uploadUrl> — browser pushes bytes straight to S3/MinIO (XHR for progress)
* 2. PUT <uploadUrl> — browser pushes bytes straight to S3 (RustFS locally) (XHR for progress)
* 3. POST /api/v1/files/{id}/finalize — server HEADs the object, transitions to Available
*
* Progress is reported via the in-state `progress` snapshot — XMLHttpRequest is used (instead of fetch)
Expand Down
2 changes: 1 addition & 1 deletion clients/dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Two options — pick whichever matches how you want to develop.

### Option A — run everything through Aspire (recommended)

The AppHost launches Postgres, Redis, MinIO, the API, the admin app, **and** this dashboard together, with `VITE_API_BASE_URL` wired via service discovery.
The AppHost launches Postgres, Redis, RustFS, the API, the admin app, **and** this dashboard together, with `VITE_API_BASE_URL` wired via service discovery.

```bash
npm install --prefix clients/dashboard # one-time
Expand Down
2 changes: 1 addition & 1 deletion clients/dashboard/src/components/file/image-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".webp", ".gif"];

/**
* ImageInput — composite control that lets a user either upload a new image
* (presigned PUT to S3/MinIO) OR paste an external URL. After a successful
* (presigned PUT to S3) OR paste an external URL. After a successful
* upload the component fetches the FileAsset metadata to retrieve the durable
* `publicUrl` and forwards it through `onChange`.
*/
Expand Down
2 changes: 1 addition & 1 deletion clients/dashboard/src/hooks/use-file-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const DEFAULT_OPTIONS = {
/**
* Orchestrates the three-step presigned-upload protocol:
* 1. POST /api/v1/files/upload-url — server mints presigned PUT + reserves a FileAsset row
* 2. PUT <uploadUrl> — browser pushes bytes straight to S3/MinIO (XHR for progress)
* 2. PUT <uploadUrl> — browser pushes bytes straight to S3 (RustFS locally) (XHR for progress)
* 3. POST /api/v1/files/{id}/finalize — server HEADs the object, transitions to Available
*
* Progress is reported via the in-state `progress` snapshot — XMLHttpRequest is used (instead of fetch)
Expand Down
6 changes: 3 additions & 3 deletions deploy/docker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ FSH_DASHBOARD_URL=https://app.example.com
FSH_DEFAULT_TENANT=root

# ── Host ports for the external proxy to point at ───────────────────
# Only FSH services publish ports. Postgres/Redis/MinIO stay
# Only FSH services publish ports. Postgres/Redis/RustFS stay
# compose-internal by default (uncomment their `ports:` blocks in
# docker-compose.yml if you need host access for psql / redis-cli).
FSH_API_PORT=8080
Expand All @@ -44,8 +44,8 @@ HANGFIRE_PASSWORD=
# ── Data plane (defaults are fine for self-hosted compose) ──────────
POSTGRES_PASSWORD=
REDIS_PASSWORD=
MINIO_ROOT_USER=
MINIO_ROOT_PASSWORD=
RUSTFS_ACCESS_KEY=
RUSTFS_SECRET_KEY=

# ── Observability (optional) ────────────────────────────────────────
# Point at an OTLP collector to ship traces/metrics. Leave blank to
Expand Down
Loading
Loading