From 772a65d896fc055465100ca790bdeec8e7aefdd9 Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Mon, 31 Aug 2026 18:33:20 -0400 Subject: [PATCH] updates --- docs/architecture.md | 176 +++++++++++++---------- docs/index.md | 15 ++ docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md | 21 ++- docs/pipeline/README.md | 15 +- docs/pipeline/ROADMAP.md | 10 +- docs/website/database.md | 118 +++++++++------ docs/website/docker.md | 26 ++-- docs/website/environment-variables.md | 36 +++-- docs/website/getting-started.md | 14 +- docs/website/testing.md | 111 +++++++------- docs/website/token-validation.md | 29 ++-- 11 files changed, 353 insertions(+), 218 deletions(-) create mode 100644 docs/index.md diff --git a/docs/architecture.md b/docs/architecture.md index c1200b45..7d5afcd8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,4 +1,13 @@ -# Architecture +--- +title: System architecture — backend, frontend, database, and deployment +last-updated: 2026-08-31 +tags: + - architecture + - backend + - frontend + - vertical-slice + - deployment +--- This document describes the architecture of The Biergarten App. @@ -54,38 +63,60 @@ repository end to end. `Features.Emails`, `Features.Locations`, and `Features.PhotoUpload` have no `Controllers/` folder; all three are invoked internally by other slices via MediatR/DI, never over HTTP: -``` -┌───────────────────────────────────────────────────────────────────────┐ -│ API.Core (thin host) │ -│ - Program.cs wiring: MediatR + AddApplicationPart per slice │ -│ - Swagger/OpenAPI, JWT auth middleware, global exception filter │ -└───────────────────────────────────────────────────────────────────────┘ - ↓ discovers controllers via AddApplicationPart -┌────────────────┬───────────────┬──────────────────┬──────────────────┬──────────────────┐ -│ Features.Users │Features. │ Features.Emails │ Features. │ Features. │ -│ │Breweries │ (no controller, │ Locations │ PhotoUpload │ -│ Controllers │ Controller │ internal only) │ (no controller, │ (no controller, │ -│ Commands/ │ Commands/ │ Commands/ │ internal only) │ internal only) │ -│ Queries + │ Queries + │ Handlers │ Repository │ Commands/ │ -│ Handlers │ Handlers │ │ │ Handlers │ -│ Repository │ Repository │ EmailDispatcher │ │ Repository │ -└────────────────┴───────────────┴──────────────────┴──────────────────┴──────────────────┘ - ↓ each slice depends only on shared/domain/infra, never on another slice -┌─────────────────────────┬─────────────────────────┬────────────────────┐ -│ Shared.Contracts │ Shared.Application │ Domain.Entities / │ -│ (ResponseBody envelope) │ (ValidationBehavior, │ Domain.Exceptions │ -│ │ cross-slice email cmds) │ │ -└─────────────────────────┴─────────────────────────┴────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────────────┐ -│ Database.Connection, Infrastructure.Jwt, Infrastructure.PasswordHashing,│ -│ Infrastructure.Email, Infrastructure.Email.Templates │ -└─────────────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────┐ -│ Database (SQL Server) │ -│ - Tables & constraints │ -└─────────────────────────────────────┘ +```mermaid +flowchart TB + subgraph HOST["API.Core (thin host)"] + direction LR + H1["Program.cs wiring:
MediatR + AddApplicationPart per slice"] + H2["Swagger / OpenAPI"] + H3["JWT auth middleware"] + H4["GlobalExceptionFilter"] + end + + subgraph SLICES["Feature Slices"] + direction LR + S1["Features.Users
Controllers, Commands/Queries,
Handlers, Repository"] + S2["Features.Breweries
Controller, Commands/Queries,
Handlers, Repository"] + S3["Features.Emails
(internal only)
Commands, Handlers,
EmailDispatcher"] + S4["Features.Locations
(internal only)
Repository"] + S5["Features.PhotoUpload
(internal only)
Commands, Handlers,
Repository"] + end + + subgraph SHARED["Shared Projects"] + direction LR + SH1["Shared.Contracts
ResponseBody envelope"] + SH2["Shared.Application
ValidationBehavior,
cross-slice email commands"] + end + + subgraph DOMAIN["Domain Layer"] + direction LR + D1["Domain.Entities"] + D2["Domain.Exceptions"] + end + + subgraph INFRA["Infrastructure Layer"] + direction LR + I1["Database.Connection"] + I2["Infrastructure.Jwt"] + I3["Infrastructure.PasswordHashing"] + I4["Infrastructure.Email /
Email.Templates"] + I5["Infrastructure.FileUpload"] + end + + DB[("Database (SQL Server)")] + + HOST -- "discovers controllers via
AddApplicationPart" --> SLICES + S1 -- "SendRegistrationEmailCommand
(via Shared.Application contract)" --> S3 + + SLICES --> SHARED + SLICES --> DOMAIN + SLICES --> INFRA + + INFRA --> DB + + style S3 stroke-dasharray: 4 3 + style S4 stroke-dasharray: 4 3 + style S5 stroke-dasharray: 4 3 ``` Slices never reference each other's project. The one cross-slice interaction @@ -147,10 +178,10 @@ no `Controllers/` folder and no MediatR handlers; it exposes (for example, `Features.Breweries`) do not yet depend on it; they run their own `CityID` existence checks inline. `Features.PhotoUpload` also has no `Controllers/` folder; its `UploadPhotoCommand` is sent by other features' -handlers (for example, a future brewery photo upload command) rather than -bound to an HTTP route directly. The one exception today is the `/upload-test` -minimal API endpoint wired directly in `API.Core/Program.cs`, a temporary route -for exercising the upload path end to end. +handlers (for example, a future brewery photo upload command) rather than bound +to an HTTP route directly. The one exception today is the `/upload-test` minimal +API endpoint wired directly in `API.Core/Program.cs`, a temporary route for +exercising the upload path end to end. **Dependencies**: @@ -196,17 +227,17 @@ whichever slices need them **Components**: - **Database.Connection**: generic ADO.NET connection/command plumbing - (`DefaultSqlConnectionFactory`, the abstract `DapperRepository` base class), not - domain-specific; each slice's own `Repository/` folder builds on this + (`DefaultSqlConnectionFactory`, the abstract `DapperRepository` base class), + not domain-specific; each slice's own `Repository/` folder builds on this - **Infrastructure.Jwt**: JWT token generation and validation - **Infrastructure.PasswordHashing**: Argon2id password hashing - **Infrastructure.Email**: Email sending capabilities (SMTP/MailKit) - **Infrastructure.Email.Templates**: Email template rendering (Razor components) - **Infrastructure.FileUpload**: `IFileStorageProvider` abstraction over - S3-compatible object storage, implemented by `S3FileStorageProvider` - (targets the SeaweedFS container in dev; any S3-compatible endpoint in - other environments) using the AWS SDK's `AmazonS3Client` with + S3-compatible object storage, implemented by `S3FileStorageProvider` (targets + the SeaweedFS container in dev; any S3-compatible endpoint in other + environments) using the AWS SDK's `AmazonS3Client` with `ForcePathStyle = true` **Dependencies**: @@ -234,8 +265,8 @@ whichever slices need them - `StateProvince` - A state/province referenced by a city - `Country` - A country referenced by a state/province - `BeerStyle` - A beer style/category (schema table exists; no repository yet) -- `BeerPost` - A beer listing tied to a brewery and style (schema table - exists; no repository yet) +- `BeerPost` - A beer listing tied to a brewery and style (schema table exists; + no repository yet) - `Photo` - A photo uploaded by a user account, written via `Features.PhotoUpload`'s `IPhotoUploadRepository` @@ -297,9 +328,9 @@ only: factory every slice's repository builds on - `Database.Connection/DapperRepository.cs`: shared abstract base class every repository extends, providing `CreateConnection()` and a - `RollbackQuietlyAsync()` helper that rolls back a transaction while - swallowing any exception the rollback itself raises, so the exception that - triggered the rollback is what propagates + `RollbackQuietlyAsync()` helper that rolls back a transaction while swallowing + any exception the rollback itself raises, so the exception that triggered the + rollback is what propagates **Benefits**: @@ -332,24 +363,23 @@ method that registers its repository and slice-internal services #### Direct SQL via Dapper/ADO.NET -**Purpose**: Keep data access explicit and colocated with the slice that owns -it +**Purpose**: Keep data access explicit and colocated with the slice that owns it **Strategy**: - No ORM; each repository issues inline SQL through Dapper, which handles - parameter binding. The one - wrinkle is the `GEOGRAPHY` column on `BreweryPostLocation`, which Dapper - can't deserialize as a UDT: reads select `CONVERT(varbinary(max), - Coordinates)` so the value comes back as a plain `byte[]` Dapper can bind. - Proximity search (`GET /api/brewery/locations/nearby`) filters and orders - rows server-side with `Coordinates.STDistance(@Origin) <= @RangeInMetres`, - avoiding a client-side distance calculation over every row. No spatial - index backs the query yet; `schema.sql` sketches one, commented out, for - when the table grows large enough to need it + parameter binding. The one wrinkle is the `GEOGRAPHY` column on + `BreweryPostLocation`, which Dapper can't deserialize as a UDT: reads select + `CONVERT(varbinary(max), Coordinates)` so the value comes back as a plain + `byte[]` Dapper can bind. Proximity search + (`GET /api/brewery/locations/nearby`) filters and orders rows server-side with + `Coordinates.STDistance(@Origin) <= @RangeInMetres`, avoiding a client-side + distance calculation over every row. No spatial index backs the query yet; + `schema.sql` sketches one, commented out, for when the table grows large + enough to need it - Referential checks (for example, "does this `CityId` exist?") are explicit - `SELECT 1 ...` existence checks in the repository method, run inside the - same transaction as the write + `SELECT 1 ...` existence checks in the repository method, run inside the same + transaction as the write - Optimistic concurrency uses each table's `RowVersion` (`ROWVERSION`) column, checked in the `UPDATE ... WHERE ... AND RowVersion = @RowVersion` clause - Repositories throw `Domain.Exceptions` types (`NotFoundException`, @@ -365,8 +395,7 @@ error-handling approach in more detail. ### Website (`web/frontend`) -The website is a React Router 7 application with server-side rendering -enabled. +The website is a React Router 7 application with server-side rendering enabled. ```text web/frontend/ @@ -409,8 +438,7 @@ web/frontend/ ### Theme system -The website uses semantic DaisyUI theme tokens backed by four Biergarten -themes: +The website uses semantic DaisyUI theme tokens backed by four Biergarten themes: - Biergarten Lager - Biergarten Stout @@ -550,8 +578,8 @@ pipeline under `tooling/pipeline/` (see [Pipeline README](pipeline/README.md)) **Seed Data**: - Countries, state/provinces, and cities (via `Features.Locations`' - `ILocationRepository`, which creates `Country`/`StateProvince` rows on - demand while resolving a city) + `ILocationRepository`, which creates `Country`/`StateProvince` rows on demand + while resolving a city) - User accounts (via `Features.Users`' repository) - Brewery posts with locations (via `Features.Breweries`' repository) @@ -601,16 +629,12 @@ healthcheck: ### Test pyramid -``` - ┌──────────────┐ - │ Integration │ ← API.Specs (Reqnroll) - │ Tests │ - ├──────────────┤ - │ Unit Tests │ ← Features.Users.Tests, Features.Breweries.Tests, - │ (per slice, │ Features.Emails.Tests, Features.PhotoUpload.Tests - │ handler │ (commands/queries/handlers, with repository/service - │ tests) │ dependencies mocked via Moq) - └──────────────┘ +```mermaid +flowchart TB + IT["Integration Tests
API.Specs (Reqnroll)"] + UT["Unit Tests (per slice, handler tests)
Features.Users.Tests, Features.Breweries.Tests,
Features.Emails.Tests, Features.PhotoUpload.Tests
(commands/queries/handlers, with repository/service
dependencies mocked via Moq)"] + + IT --- UT ``` **Strategy**: diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..610028f0 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,15 @@ +--- +title: The Biergarten App +last-updated: 2026-08-31 +tags: + - overview + - index +--- + +Welcome to the documentation for The Biergarten App. + +## Documentation Sections + +- **[Architecture](architecture.md)** — System design and components +- **[Website](website/getting-started.md)** — Backend and frontend documentation +- **[Pipeline](pipeline/README.md)** — Data pipeline and ETL processes diff --git a/docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md b/docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md index b7096e7b..1bc5b13a 100644 --- a/docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md +++ b/docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md @@ -1,4 +1,13 @@ -# Ethics, bias, and known issues +--- +title: Pipeline ethics, bias, and known issues +last-updated: 2026-08-31 +tags: + - pipeline + - ethics + - bias + - known-issues + - ai-generated +--- This document covers the ethical context of the Biergarten Pipeline's output, the model's biases, and known issues including hallucinated brewing science and @@ -290,11 +299,11 @@ Each brewery and user address carries a longitude/latitude pair, sampled uniformly at random within a 5 km disc centred on the city's curated `latitude`/`longitude` from `locations.json`. -The resulting point is **plausible by construction**, always falling within -the city's vicinity, but it is not looked up from any real address registry. -It does not correspond to a real address, business, or resident, and should -not be treated as accurate geocoding data. This is consistent with the rest of -the dataset: fixture data for a proof-of-concept, not a source of truth. +The resulting point is **plausible by construction**, always falling within the +city's vicinity, but it is not looked up from any real address registry. It does +not correspond to a real address, business, or resident, and should not be +treated as accurate geocoding data. This is consistent with the rest of the +dataset: fixture data for a proof-of-concept, not a source of truth. --- diff --git a/docs/pipeline/README.md b/docs/pipeline/README.md index 626bc46f..e223be91 100644 --- a/docs/pipeline/README.md +++ b/docs/pipeline/README.md @@ -1,4 +1,13 @@ -# Biergarten Pipeline +--- +title: Biergarten Pipeline — C++ CLI for generating seed data +last-updated: 2026-08-31 +tags: + - pipeline + - cpp + - llm + - seed-data + - docker +--- A C++20 command-line pipeline that samples city records from local JSON, enriches each with Wikipedia context, and generates bilingual brewery names and @@ -436,8 +445,8 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present. Paths below are relative to `tooling/pipeline/`. - `src/main.cc`: argument parsing and DI composition root. -- `src/biergarten_pipeline_orchestrator/`: orchestration, sampling, logging, - and export. +- `src/biergarten_pipeline_orchestrator/`: orchestration, sampling, logging, and + export. - `src/services/curated_data/`: `CuratedJsonDataService`, the file-backed `ICuratedDataService`, and `MockCuratedDataService`, the in-memory `ICuratedDataService` used in `--mocked` runs. diff --git a/docs/pipeline/ROADMAP.md b/docs/pipeline/ROADMAP.md index 71e01f06..858dd2b2 100644 --- a/docs/pipeline/ROADMAP.md +++ b/docs/pipeline/ROADMAP.md @@ -1,4 +1,12 @@ -# Pipeline Roadmap — Reaching the Planned Architecture +--- +title: Pipeline roadmap — reaching the planned architecture +last-updated: 2026-08-31 +tags: + - pipeline + - roadmap + - cpp + - concurrency +--- This is the engineering breakdown for closing the gap between the **current** pipeline and the **planned** architecture documented in diff --git a/docs/website/database.md b/docs/website/database.md index b763d868..55d71076 100644 --- a/docs/website/database.md +++ b/docs/website/database.md @@ -1,4 +1,13 @@ -# Database +--- +title: SQL Server database schema, migrations, and error handling +last-updated: 2026-08-31 +tags: + - sql + - repository + - database + - sql-server + - schema +--- SQL Server 2022 schema, migration approach, and the app's SQL error-handling convention. For the vertical-slice code that talks to this schema, see @@ -7,15 +16,36 @@ convention. For the vertical-slice code that talks to this schema, see ## Schema The full schema lives under -`web/backend/Database/Database.Migrations/scripts/01-schema/`, one numbered -script per table (`01-UserAccount.sql`, `02-Photo.sql`, ...), run in order by -`Database.Migrations` (DbUp) on startup. Each table is queried directly by the -owning feature slice's repository (see +`web/backend/Database/Database.Migrations/scripts/01-schema/` with one numbered +script per table. + +``` +└── 01-schema + ├── 01-UserAccount.sql + ├── 02-Photo.sql + ├── 03-UserVerification.sql + ├── 04-UserCredential.sql + ├── 05-UserFollow.sql + ├── 06-Country.sql + ├── 07-StateProvince.sql + ├── 08-City.sql + ├── 09-BreweryPost.sql + ├── 10-BreweryPostLocation.sql + ├── 11-BreweryPostPhoto.sql + ├── 12-BeerStyle.sql + ├── 13-BeerPost.sql + ├── 14-BeerPostPhoto.sql + ├── 15-BeerPostComment.sql + ├── 16-UserProfile.sql + └── 17-UserAvatar.sql +``` + +Each script is run in order by `Database.Migrations` (DbUp) on startup. Each +table is queried directly by the owning feature slice's repository (see [Direct SQL via Dapper/ADO.NET](../architecture.md#direct-sql-via-dapperadonet)). -A generated DBML copy of the schema (for import into a diagramming tool such -as dbdiagram.io) lives at -`web/backend/Database/Database.Migrations/schema.dbml`. +A generated DBML copy of the schema (for import into a diagramming tool such as +dbdiagram.io) lives at `web/backend/Database/Database.Migrations/schema.dbml`. All tables use a `UNIQUEIDENTIFIER` primary key defaulting to `NEWID()`, and most carry a `RowVersion ROWVERSION` column used for optimistic concurrency on @@ -23,44 +53,52 @@ updates (`UPDATE ... WHERE ... AND RowVersion = @RowVersion`). ### Tables backed by the API today -| Table | Backed by | Notes | -| --------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `UserAccount` | `Domain.Entities.UserAccount`, `Features.Users` | `Username` and `Email` are unique | -| `UserCredential` | `Domain.Entities.UserCredential`, `Features.Users` | Argon2id password hash; `IsRevoked`/`RevokedAt` track credential rotation, not JWTs | -| `UserVerification` | `Domain.Entities.UserVerification`, `Features.Users` | Written when a user confirms their email | -| `UserProfile` | `Domain.Entities.UserProfile`, `Features.Users` | 1:1 with `UserAccount`; holds the user's biography; written via `IUserProfileRepository` | -| `UserAvatar` | `Domain.Entities.UserAvatar`, `Features.Users` | Links a `UserProfile` to a `Photo` as its avatar; written via `IUserProfileRepository.SaveAvatarAsync` | -| `BreweryPost` | `Domain.Entities.BreweryPost`, `Features.Breweries` | A user-submitted brewery listing | -| `BreweryPostLocation` | `Domain.Entities.BreweryPostLocation`, `Features.Breweries` | 1:1 with `BreweryPost`; `Coordinates` is a `GEOGRAPHY` column, read via a plain Dapper query that selects `CONVERT(varbinary(max), Coordinates)`, since Dapper can't deserialize SQL Server UDTs directly | -| `City` | `Domain.Entities.City`, `Features.Locations` | Referenced by `BreweryPostLocation.CityID`; written via `ILocationRepository`, read via `ILocationRepository` and `IBreweryRepository`'s location joins, currently written only by `Database.Seed` | -| `StateProvince` | `Domain.Entities.StateProvince`, `Features.Locations` | Referenced by `City.StateProvinceID`; created on demand by `ILocationRepository.GetOrCreateCityIdAsync`, read via `IBreweryRepository`'s location joins | -| `Country` | `Domain.Entities.Country`, `Features.Locations` | Referenced by `StateProvince.CountryID`; created on demand by `ILocationRepository.GetOrCreateCityIdAsync`, read via `IBreweryRepository`'s location joins | -| `Photo` | `Domain.Entities.Photo`, `Features.PhotoUpload` | Written via `IPhotoUploadRepository.CreateAsync`, sent as an `UploadPhotoCommand` from other features' handlers; not yet seeded by `Database.Seed` | +| Table | Backed by | Notes | +| --------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `UserAccount` | `Domain.Entities.UserAccount`, `Features.Users` | `Username` and `Email` are unique | +| `UserCredential` | `Domain.Entities.UserCredential`, `Features.Users` | Argon2id password hash; `IsRevoked`/`RevokedAt` track credential rotation, not JWTs | +| `UserVerification` | `Domain.Entities.UserVerification`, `Features.Users` | Written when a user confirms their email | +| `UserProfile` | `Domain.Entities.UserProfile`, `Features.Users` | 1:1 with `UserAccount`; holds the user's biography; written via `IUserProfileRepository` | +| `UserAvatar` | `Domain.Entities.UserAvatar`, `Features.Users` | Links a `UserProfile` to a `Photo` as its avatar; written via `IUserProfileRepository.SaveAvatarAsync` | +| `BreweryPost` | `Domain.Entities.BreweryPost`, `Features.Breweries` | A user-submitted brewery listing | +| `BreweryPostLocation` | `Domain.Entities.BreweryPostLocation`, `Features.Breweries` | 1:1 with `BreweryPost`; `Coordinates` is a `GEOGRAPHY` column, read via a plain Dapper query that selects `CONVERT(varbinary(max), Coordinates)`, since Dapper can't deserialize SQL Server UDTs directly | +| `City` | `Domain.Entities.City`, `Features.Locations` | Referenced by `BreweryPostLocation.CityID`; written via `ILocationRepository`, read via `ILocationRepository` and `IBreweryRepository`'s location joins, currently written only by `Database.Seed` | +| `StateProvince` | `Domain.Entities.StateProvince`, `Features.Locations` | Referenced by `City.StateProvinceID`; created on demand by `ILocationRepository.GetOrCreateCityIdAsync`, read via `IBreweryRepository`'s location joins | +| `Country` | `Domain.Entities.Country`, `Features.Locations` | Referenced by `StateProvince.CountryID`; created on demand by `ILocationRepository.GetOrCreateCityIdAsync`, read via `IBreweryRepository`'s location joins | +| `Photo` | `Domain.Entities.Photo`, `Features.PhotoUpload` | Written via `IPhotoUploadRepository.CreateAsync`, sent as an `UploadPhotoCommand` from other features' handlers; not yet seeded by `Database.Seed` | ### Tables defined in the schema but not yet wired to a feature slice These exist so the schema can support planned functionality, but have no repository or seed data yet: -| Table | Purpose (from schema) | -| ------------------ | -------------------------------------------------------------------------------------------------------- | -| `UserFollow` | One user following another (`CannotFollowOwnAccount` check constraint) | -| `BeerStyle` | A beer style/category; has a `Domain.Entities.BeerStyle` type, but no repository yet | +| Table | Purpose (from schema) | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `UserFollow` | One user following another (`CannotFollowOwnAccount` check constraint) | +| `BeerStyle` | A beer style/category; has a `Domain.Entities.BeerStyle` type, but no repository yet | | `BeerPost` | A beer listing, linked to a `BreweryPost` and `BeerStyle`; `ABV`/`IBU` are constrained (`0-67`, `0-120`); has a `Domain.Entities.BeerPost` type, but no repository yet | -| `BeerPostPhoto` | Links a `BeerPost` to a `Photo` | -| `BeerPostComment` | A comment + 1-5 `Rating` on a `BeerPost` | -| `BreweryPostPhoto` | Links a `BreweryPost` to a `Photo` | +| `BeerPostPhoto` | Links a `BeerPost` to a `Photo` | +| `BeerPostComment` | A comment + 1-5 `Rating` on a `BeerPost` | +| `BreweryPostPhoto` | Links a `BreweryPost` to a `Photo` | -## Migrations +## Managing Migrations (DbUp) -**Tool**: DbUp, run by the `Database.Migrations` project/container. +Migrations are handled by the `Database.Migrations` container using DbUp. +Scripts are placed in the `scripts/` directory, applied in order, and tracked so +that each one runs only once. -Scripts live under `scripts/`, applied in order and tracked so a script never -re-runs once it has succeeded. Today that's `01-schema/`, with one script per -table, numbered so a table's foreign keys always point at an earlier-numbered -script. When the schema needs to change, add a new numbered script rather than -editing an existing one in place: DbUp's tracking is per-script, so an edited -already-applied script never re-runs against existing databases. +**Note:** The schema is currently considered volatile. At this stage of +development, fresh database instances are provisioned for both development and +testing, so no production data exists yet. + +### How to Make Schema Changes + +- **Now (no production data):** Update the schema by editing the original DDL + statements directly. +- **Later (with production data):** Add a new, numbered script for any schema + changes. Never edit an existing script in place — DbUp tracks completion by + script name, so modifying an already-applied script means the changes will be + ignored on existing databases. ## Data seeding @@ -81,7 +119,7 @@ repository (`Features.PhotoUpload`) but no seed data. 409, 400, ...); an unmapped `SqlException` (for example, a genuine connectivity or constraint-violation failure) maps to 503. - - One exception: `Features.Users`' `DapperUserStore` does catch - `SqlException` directly, to detect a duplicate-key race (SQL Server error - 2601 or 2627) when two requests try to verify the same account - concurrently; see `DapperUserStore.IsDuplicateKeyViolation`. + - One exception: `Features.Users`' `DapperUserStore` does catch `SqlException` + directly, to detect a duplicate-key race (SQL Server error 2601 or 2627) + when two requests try to verify the same account concurrently; see + `DapperUserStore.IsDuplicateKeyViolation`. diff --git a/docs/website/docker.md b/docs/website/docker.md index fbfaf271..3340f641 100644 --- a/docs/website/docker.md +++ b/docs/website/docker.md @@ -1,4 +1,11 @@ -# Docker guide +--- +title: Docker deployment, configuration, and troubleshooting +last-updated: 2026-08-31 +tags: + - docker + - docker-compose + - deployment +--- This document covers Docker deployment, configuration, and troubleshooting for The Biergarten App. @@ -114,9 +121,9 @@ docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v `wait` blocks until `api.specs`, `unit.tests`, and `frontend.tests` have all stopped, regardless of which one finishes first; `up --abort-on-container-exit` -isn't used here because the one-shot `database.migrations`/`database.seed` -jobs and `frontend.tests` (no database dependency) can exit before the other -test containers, which would end the run prematurely. See +isn't used here because the one-shot `database.migrations`/`database.seed` jobs +and `frontend.tests` (no database dependency) can exit before the other test +containers, which would end the run prematurely. See [Testing](testing.md#running-tests-with-docker-recommended) for details. ### 3. Production (`docker-compose.prod.yaml`) @@ -131,8 +138,8 @@ test containers, which would end the run prematurely. See - Health checks enabled - Restart policies (unless-stopped) - Security hardening -- Only `frontend` publishes a port to the host; `sqlserver` and `api.core` - are reachable only from other containers on `prodnet` +- Only `frontend` publishes a port to the host; `sqlserver` and `api.core` are + reachable only from other containers on `prodnet` **Services**: @@ -206,8 +213,8 @@ graph TD E[seaweedfs: container started] --> D ``` -In development, `api.core` also depends on `seaweedfs` (`condition: -service_started`, not a health check) alongside `database.seed`. +In development, `api.core` also depends on `seaweedfs` +(`condition: service_started`, not a health check) alongside `database.seed`. **Health Check Example** (SQL Server): @@ -241,8 +248,7 @@ api.core: - `sqlserverdata-dev` - Database files persist between restarts - `nuget-cache-dev` - NuGet package cache (speeds up builds) -- `seaweedfsdata-dev` - SeaweedFS object storage data persists between - restarts +- `seaweedfsdata-dev` - SeaweedFS object storage data persists between restarts **Testing**: diff --git a/docs/website/environment-variables.md b/docs/website/environment-variables.md index 7c5d74c2..09dea3ac 100644 --- a/docs/website/environment-variables.md +++ b/docs/website/environment-variables.md @@ -1,4 +1,12 @@ -# Environment variables +--- +title: Environment variables for the backend, frontend, and Docker +last-updated: 2026-08-31 +tags: + - environment-variables + - configuration + - secrets + - docker +--- This document covers the environment variables used by the Biergarten stack. @@ -37,9 +45,9 @@ Environment-specific `.env` files loaded via `env_file:` in docker-compose.yaml: secret-bearing values (`DB_PASSWORD`, `ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET`, `SESSION_SECRET`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `SEAWEEDFS_ACCESS_KEY_ID`, -`SEAWEEDFS_SECRET_ACCESS_KEY`) -with freshly randomized ones via `openssl`. Non-secret values are left -untouched. The output file is written with `600` permissions. +`SEAWEEDFS_SECRET_ACCESS_KEY`) with freshly randomized ones via `openssl`. +Non-secret values are left untouched. The output file is written with `600` +permissions. ```bash cd web @@ -171,12 +179,12 @@ DOTNET_RUNNING_IN_CONTAINER=true # Flag for container execution ``` `ASPNETCORE_URLS` is hardcoded directly in `docker-compose.dev.yaml` and -`docker-compose.prod.yaml` (not sourced from the `.env` file), so it's -required only for Docker. Outside Docker, `dotnet run` falls back to the -`http` profile in `API.Core/Properties/launchSettings.json` -(`http://localhost:5069`), so manual backend setup (see -[Getting Started](getting-started.md#manual-backend-setup)) doesn't need to -set it. +`docker-compose.prod.yaml` (not sourced from the `.env` file), so it's required +only for Docker. Outside Docker, `dotnet run` falls back to the `http` profile +in `API.Core/Properties/launchSettings.json` (`http://localhost:5069`), so +manual backend setup (see +[Getting Started](getting-started.md#manual-backend-setup)) doesn't need to set +it. ## Frontend variables (`web/frontend`) @@ -191,8 +199,8 @@ PORT=3000 # Port the built server listens on The `frontend` Docker service hardcodes `API_BASE_URL` to `http://api.core:8080` (the backend's in-network service name) and `PORT` to `3000`; only -`SESSION_SECRET` is sourced from the env file, and `NODE_ENV` is set per -compose file (`development` in dev, `production` in prod). +`SESSION_SECRET` is sourced from the env file, and `NODE_ENV` is set per compose +file (`development` in dev, `production` in prod). ### Frontend variable details @@ -357,8 +365,8 @@ Variables are validated at startup: - Missing required variables (`ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET`, `SMTP_HOST`, `SMTP_FROM_EMAIL`, `SEAWEEDFS_SERVICE_URL`, `SEAWEEDFS_ACCESS_KEY_ID`, - `SEAWEEDFS_SECRET_ACCESS_KEY`, `SEAWEEDFS_BUCKET`, DB connection values) - cause the application to fail with an `InvalidOperationException` + `SEAWEEDFS_SECRET_ACCESS_KEY`, `SEAWEEDFS_BUCKET`, DB connection values) cause + the application to fail with an `InvalidOperationException` - No minimum length is enforced on the token secrets in code; the "minimum 32 characters" guidance above is a recommendation, not an enforced check diff --git a/docs/website/getting-started.md b/docs/website/getting-started.md index ce01eb1b..721d5607 100644 --- a/docs/website/getting-started.md +++ b/docs/website/getting-started.md @@ -1,4 +1,11 @@ -# Getting started +--- +title: Getting started — local setup for backend and frontend +last-updated: 2026-08-31 +tags: + - getting-started + - setup + - onboarding +--- This guide covers local setup for the Biergarten stack: the .NET backend in `web/backend` and the React Router frontend in `web/frontend`. @@ -58,9 +65,8 @@ section for details on `generate-env.sh`. docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up -d ``` -This starts SQL Server, migrations, seeding, the API, Mailpit (local dev -SMTP), and SeaweedFS (local dev S3-compatible object storage for photo -uploads). +This starts SQL Server, migrations, seeding, the API, Mailpit (local dev SMTP), +and SeaweedFS (local dev S3-compatible object storage for photo uploads). Available endpoints: diff --git a/docs/website/testing.md b/docs/website/testing.md index 9f936c48..7d716ddd 100644 --- a/docs/website/testing.md +++ b/docs/website/testing.md @@ -1,4 +1,13 @@ -# Testing +--- +title: Testing strategy — unit, integration, and Storybook suites +last-updated: 2026-08-31 +tags: + - testing + - ci + - xunit + - reqnroll + - storybook +--- This document describes the testing strategy and how to run tests for The Biergarten App. @@ -10,10 +19,9 @@ The project uses a multi-layered testing approach across backend and frontend: - **API.Specs** - BDD integration tests using Reqnroll (Gherkin), run against a live, seeded database - **Features.\*.Tests** - One unit test project per backend feature slice - (`Features.Users.Tests`, `Features.Breweries.Tests`, - `Features.Emails.Tests`, `Features.PhotoUpload.Tests`), covering that - slice's command/query handlers, with dependencies mocked via Moq (no real - database required) + (`Features.Users.Tests`, `Features.Breweries.Tests`, `Features.Emails.Tests`, + `Features.PhotoUpload.Tests`), covering that slice's command/query handlers, + with dependencies mocked via Moq (no real database required) - **Storybook Vitest project** - Browser-based interaction tests for shared website stories - **Storybook Playwright suite** - Browser checks against Storybook-rendered @@ -42,8 +50,8 @@ This command: `wait` blocks until `api.specs`, `unit.tests`, and `frontend.tests` have all stopped, regardless of which one finishes first. Long-running services (`sqlserver`, `seaweedfs`) and the one-shot `database.migrations` / -`database.seed` jobs keep running in the background alongside them and are -torn down separately (see below); `wait` doesn't touch them. +`database.seed` jobs keep running in the background alongside them and are torn +down separately (see below); `wait` doesn't touch them. ### View test results @@ -175,8 +183,8 @@ The default (`headless: true`) is set in `web/frontend/vite.config.ts`. **Features.Breweries.Tests**: - Brewery create/update/delete commands and get-by-id/get-all queries -- Brewery location queries: all locations, and locations within a given range - of a coordinate +- Brewery location queries: all locations, and locations within a given range of + a coordinate **Features.Emails.Tests**: @@ -186,9 +194,8 @@ The default (`headless: true`) is set in `web/frontend/vite.config.ts`. - Upload command handler: storage key construction, content type passthrough, and photo persistence via the mocked storage provider and repository -- Upload validator: min/max file size bounds, and PNG/JPEG/WebP signature - checks (including rejection of files with a mismatched or missing - signature) +- Upload validator: min/max file size bounds, and PNG/JPEG/WebP signature checks + (including rejection of files with a mismatched or missing signature) **Frontend UI Coverage**: @@ -295,8 +302,8 @@ Features.Users.Tests/ Each of the other slices (`Features.Breweries.Tests`, `Features.Emails.Tests`, `Features.PhotoUpload.Tests`) follows the same shape: a `Commands/`/`Queries/` -folder with one test file per handler (`Features.PhotoUpload.Tests` also adds -a validator test file alongside its handler test). +folder with one test file per handler (`Features.PhotoUpload.Tests` also adds a +validator test file alongside its handler test). ## Writing tests @@ -337,10 +344,10 @@ Scenario: User login with valid credentials ## Continuous integration -The `.github/workflows/tests.yml` GitHub Actions workflow runs on every push -and pull request to `main`. It runs the same test Docker Compose -configuration used locally, so backend and frontend tests execute in the same -containerized environment in CI as they do on a developer machine: +The `.github/workflows/tests.yml` GitHub Actions workflow runs on every push and +pull request to `main`. It runs the same test Docker Compose configuration used +locally, so backend and frontend tests execute in the same containerized +environment in CI as they do on a developer machine: ```bash # CI/CD command @@ -351,16 +358,15 @@ docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v ``` The workflow inspects the exit code of each test container -(`test-env-api-specs`, `test-env-unit-tests`, `test-env-frontend-tests`) -after `wait` returns and fails the job if any of them is non-zero. -`./test-results/` is uploaded as a build artifact regardless of outcome. +(`test-env-api-specs`, `test-env-unit-tests`, `test-env-frontend-tests`) after +`wait` returns and fails the job if any of them is non-zero. `./test-results/` +is uploaded as a build artifact regardless of outcome. -`up -d` + `wait` keeps container teardown independent of any single -container's exit. The one-shot `database.migrations`/`database.seed` jobs -exit 0 partway through the run, and `frontend.tests` (which has no database -dependency) reliably finishes before `api.specs`/`unit.tests` start; because -`wait` blocks on all three test containers, neither early exit ends the run -prematurely. +`up -d` + `wait` keeps container teardown independent of any single container's +exit. The one-shot `database.migrations`/`database.seed` jobs exit 0 partway +through the run, and `frontend.tests` (which has no database dependency) +reliably finishes before `api.specs`/`unit.tests` start; because `wait` blocks +on all three test containers, neither early exit ends the run prematurely. Exit codes: @@ -368,9 +374,9 @@ Exit codes: - Non-zero - Test failures occurred The frontend's Storybook Vitest and Playwright suites run inside the -`frontend.tests` container (see [Docker Guide](docker.md)) as part of that -same compose run, so no separate frontend CI step is needed. To run them -locally without Docker: +`frontend.tests` container (see [Docker Guide](docker.md)) as part of that same +compose run, so no separate frontend CI step is needed. To run them locally +without Docker: ```bash cd web/frontend @@ -381,9 +387,9 @@ npm run test:storybook:playwright ### Running the workflow locally with `act` -[`act`](https://github.com/nektos/act) replays `.github/workflows/*.yml` on -your own machine using Docker, so you can reproduce a CI run (or a fix for -one) without pushing a branch. +[`act`](https://github.com/nektos/act) replays `.github/workflows/*.yml` on your +own machine using Docker, so you can reproduce a CI run (or a fix for one) +without pushing a branch. **Install** (macOS): @@ -401,34 +407,31 @@ act push \ --container-architecture linux/amd64 ``` -- `-j containerized-tests` runs only that job (there's currently just the - one). -- `-P ubuntu-latest=catthehacker/ubuntu:act-latest` pins the runner image - act uses to impersonate `ubuntu-latest`. Without it, act's first run - prompts interactively to choose a default image size, which hangs/fails - under a non-interactive shell. +- `-j containerized-tests` runs only that job (there's currently just the one). +- `-P ubuntu-latest=catthehacker/ubuntu:act-latest` pins the runner image act + uses to impersonate `ubuntu-latest`. Without it, act's first run prompts + interactively to choose a default image size, which hangs/fails under a + non-interactive shell. - `--container-architecture linux/amd64` is required on Apple Silicon: the - `sqlserver` service's image is amd64-only, and act needs to know to - emulate that platform for the whole job container, not just that one - service. + `sqlserver` service's image is amd64-only, and act needs to know to emulate + that platform for the whole job container, not just that one service. - No secrets or `.actrc` are needed: `generate-env.sh` creates its own `.env.test` with freshly randomized values, same as in real CI. **Known act-only limitation**: the "Upload test results" step -(`actions/upload-artifact@v4`) fails locally with `Unable to get the -ACTIONS_RUNTIME_TOKEN env variable`. act doesn't provide a real Actions -artifact backend, so this step, and only this step, is expected to fail -under act even when everything else passes. It works normally on +(`actions/upload-artifact@v4`) fails locally with +`Unable to get the ACTIONS_RUNTIME_TOKEN env variable`. act doesn't provide a +real Actions artifact backend, so this step, and only this step, is expected to +fail under act even when everything else passes. It works normally on GitHub-hosted runners. -**Stale local state**: `docker-compose.test.yaml` uses fixed container -names and named volumes (`sqlserverdata-test`, `seaweedfsdata-test`) scoped -to the `web` compose project. If a previous local run (via `act` or a -manual `docker compose` invocation) didn't get torn down, its SQL Server -volume can persist with an old `SA_PASSWORD` baked in, while a fresh -`.env.test` generates a new one each run, causing `Login failed for user -'sa'` errors that look like a test bug but are really a leftover volume. -Clear it before re-running: +**Stale local state**: `docker-compose.test.yaml` uses fixed container names and +named volumes (`sqlserverdata-test`, `seaweedfsdata-test`) scoped to the `web` +compose project. If a previous local run (via `act` or a manual `docker compose` +invocation) didn't get torn down, its SQL Server volume can persist with an old +`SA_PASSWORD` baked in, while a fresh `.env.test` generates a new one each run, +causing `Login failed for user 'sa'` errors that look like a test bug but are +really a leftover volume. Clear it before re-running: ```bash docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v diff --git a/docs/website/token-validation.md b/docs/website/token-validation.md index 390fba7a..a9a21dee 100644 --- a/docs/website/token-validation.md +++ b/docs/website/token-validation.md @@ -1,4 +1,12 @@ -# Token validation architecture +--- +title: JWT token validation architecture +last-updated: 2026-08-31 +tags: + - jwt + - authentication + - security + - tokens +--- ## Overview @@ -64,10 +72,9 @@ Token generation and validation live on the same slice-internal service. `TokenService` is registered by `Features.Users`' own `AddFeaturesUsers()` extension method, except for the lower-level `ITokenInfrastructure` (JWT -signing/verification) it -depends on, which is registered by the host (`API.Core/Program.cs`) since -`JwtAuthenticationHandler` (host-level auth middleware) also depends on it -directly. +signing/verification) it depends on, which is registered by the host +(`API.Core/Program.cs`) since `JwtAuthenticationHandler` (host-level auth +middleware) also depends on it directly. ### Integration points @@ -77,14 +84,16 @@ directly. 1. Receives confirmation token from user via `ConfirmUserCommand` 2. Calls `ITokenService.ValidateConfirmationTokenAsync()` -3. Extracts user ID from validated token and looks it up via `UserManager` -4. Marks the account confirmed via `IUserEmailStore.SetEmailConfirmedAsync()`, - then persists it via `UserManager.UpdateAsync()` +3. Extracts user ID from validated token and looks it up via + `UserManager` +4. Marks the account confirmed via + `IUserEmailStore.SetEmailConfirmedAsync()`, then persists it + via `UserManager.UpdateAsync()` 5. Returns confirmation result `UserManager` and `IUserEmailStore` are -ASP.NET Core Identity's abstractions, not EF Core's default Identity store: -the app backs them with its own `DapperUserStore` (see +ASP.NET Core Identity's abstractions, not EF Core's default Identity store: the +app backs them with its own `DapperUserStore` (see [Database](database.md#database-error-handling)), which persists against the existing `UserAccount`/`UserCredential`/`UserVerification` tables via Dapper, consistent with the rest of the app's repository pattern.