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.