Cleanup prose in written documentation

This commit is contained in:
Aaron Po
2026-08-31 17:23:46 -04:00
parent f28f188a54
commit 30995f6f40
9 changed files with 141 additions and 113 deletions

View File

@@ -1,11 +1,10 @@
# Architecture # Architecture
This document describes the active architecture of The Biergarten App. This document describes the architecture of The Biergarten App.
## High-level overview ## High-level overview
The Biergarten App is a monorepo with a clear split between the backend and the The Biergarten App is a monorepo split between the backend and the website:
active website:
- **Backend**: .NET 10 Web API with SQL Server, organized as vertical feature - **Backend**: .NET 10 Web API with SQL Server, organized as vertical feature
slices with MediatR slices with MediatR
@@ -13,8 +12,8 @@ active website:
- **Architecture Style**: Vertical-slice backend plus server-rendered React - **Architecture Style**: Vertical-slice backend plus server-rendered React
frontend frontend
The legacy Next.js frontend has been retained in `archive/next-js-web-app/` for `archive/next-js-web-app/` contains an archived Next.js frontend, kept for
reference only. reference.
## Diagrams ## Diagrams
@@ -24,11 +23,25 @@ For visual representations, see:
architecture diagram architecture diagram
- [deployment.svg](website/diagrams-out/deployment.svg) - Docker deployment - [deployment.svg](website/diagrams-out/deployment.svg) - Docker deployment
diagram diagram
- [authentication-flow.svg](website/diagrams-out/authentication-flow.svg) -
Authentication workflow
- [database-schema.svg](website/diagrams-out/database-schema.svg) - Database - [database-schema.svg](website/diagrams-out/database-schema.svg) - Database
relationships relationships
Authentication workflow diagrams, one per flow, under
`website/diagrams-out/auth/`:
- [registration.svg](website/diagrams-out/auth/registration.svg)
- [login.svg](website/diagrams-out/auth/login.svg)
- [refresh-token.svg](website/diagrams-out/auth/refresh-token.svg)
- [confirm-user.svg](website/diagrams-out/auth/confirm-user.svg)
- [resend-confirmation.svg](website/diagrams-out/auth/resend-confirmation.svg)
- [update-username.svg](website/diagrams-out/auth/update-username.svg)
- [update-email.svg](website/diagrams-out/auth/update-email.svg)
- [update-password.svg](website/diagrams-out/auth/update-password.svg)
- [update-profile.svg](website/diagrams-out/auth/update-profile.svg)
- [delete-account.svg](website/diagrams-out/auth/delete-account.svg)
- [error-handling.svg](website/diagrams-out/auth/error-handling.svg) - shared
`GlobalExceptionFilter` mapping used by all flows above
## Backend architecture ## Backend architecture
### Vertical-slice architecture pattern ### Vertical-slice architecture pattern
@@ -38,7 +51,7 @@ technical layers. Each feature (`Features.Users`, `Features.Breweries`,
`Features.Emails`, `Features.Locations`, `Features.PhotoUpload`) is a single `Features.Emails`, `Features.Locations`, `Features.PhotoUpload`) is a single
project that owns its own MediatR commands/queries/handlers, validators, and project that owns its own MediatR commands/queries/handlers, validators, and
repository end to end. `Features.Emails`, `Features.Locations`, and repository end to end. `Features.Emails`, `Features.Locations`, and
`Features.PhotoUpload` have no `Controllers/` folder — all three are invoked `Features.PhotoUpload` have no `Controllers/` folder; all three are invoked
internally by other slices via MediatR/DI, never over HTTP: internally by other slices via MediatR/DI, never over HTTP:
``` ```
@@ -128,12 +141,12 @@ own
`Features.Emails` has no `Controllers/` folder. It's invoked only via MediatR `Features.Emails` has no `Controllers/` folder. It's invoked only via MediatR
commands sent from other slices, never over HTTP. `Features.Locations` also has commands sent from other slices, never over HTTP. `Features.Locations` also has
no `Controllers/` folder and no MediatR handlers — it exposes no `Controllers/` folder and no MediatR handlers; it exposes
`ILocationRepository` directly as a plain service, currently consumed only by `ILocationRepository` directly as a plain service, currently consumed only by
`Database.Seed` to look up cities while seeding brewery locations. Other slices `Database.Seed` to look up cities while seeding brewery locations. Other slices
(for example, `Features.Breweries`) do not yet depend on it; they run their own (for example, `Features.Breweries`) do not yet depend on it; they run their own
`CityID` existence checks inline. `Features.PhotoUpload` also has no `CityID` existence checks inline. `Features.PhotoUpload` also has no
`Controllers/` folder — its `UploadPhotoCommand` is sent by other features' `Controllers/` folder; its `UploadPhotoCommand` is sent by other features'
handlers (for example, a future brewery photo upload command) rather than 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` 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 minimal API endpoint wired directly in `API.Core/Program.cs`, a temporary route
@@ -320,12 +333,12 @@ method that registers its repository and slice-internal services
#### Direct SQL via Dapper/ADO.NET #### Direct SQL via Dapper/ADO.NET
**Purpose**: Keep data access explicit and colocated with the slice that owns **Purpose**: Keep data access explicit and colocated with the slice that owns
it, without an ORM's change-tracking or mapping magic it
**Strategy**: **Strategy**:
- No ORM (Entity Framework not used); each repository issues inline SQL - No ORM; each repository issues inline SQL through Dapper, which handles
through Dapper, with no manual `IDbCommand` parameter binding. The one parameter binding. The one
wrinkle is the `GEOGRAPHY` column on `BreweryPostLocation`, which Dapper wrinkle is the `GEOGRAPHY` column on `BreweryPostLocation`, which Dapper
can't deserialize as a UDT: reads select `CONVERT(varbinary(max), can't deserialize as a UDT: reads select `CONVERT(varbinary(max),
Coordinates)` so the value comes back as a plain `byte[]` Dapper can bind. Coordinates)` so the value comes back as a plain `byte[]` Dapper can bind.
@@ -334,26 +347,25 @@ it, without an ORM's change-tracking or mapping magic
avoiding a client-side distance calculation over every row. No spatial avoiding a client-side distance calculation over every row. No spatial
index backs the query yet; `schema.sql` sketches one, commented out, for index backs the query yet; `schema.sql` sketches one, commented out, for
when the table grows large enough to need it when the table grows large enough to need it
- Referential checks that a stored procedure used to perform (e.g. "does this - Referential checks (for example, "does this `CityId` exist?") are explicit
`CityId` exist?") are now explicit `SELECT 1 ...` existence checks in the `SELECT 1 ...` existence checks in the repository method, run inside the
repository method, run inside the same transaction as the write same transaction as the write
- Optimistic concurrency uses each table's `RowVersion` (`ROWVERSION`) column, - Optimistic concurrency uses each table's `RowVersion` (`ROWVERSION`) column,
checked in the `UPDATE ... WHERE ... AND RowVersion = @RowVersion` clause checked in the `UPDATE ... WHERE ... AND RowVersion = @RowVersion` clause
- Repositories throw `Domain.Exceptions` types (`NotFoundException`, - Repositories throw `Domain.Exceptions` types (`NotFoundException`,
`ConflictException`, ...) directly when a check fails, rather than relying on `ConflictException`, ...) directly when a check fails; `API.Core`'s
a database-side `THROW`; `API.Core`'s `GlobalExceptionFilter` maps those `GlobalExceptionFilter` maps those exception types to HTTP status codes
exception types to HTTP status codes
- Application focuses on orchestration; the database enforces integrity via - Application focuses on orchestration; the database enforces integrity via
keys, `CHECK` constraints, and cascades keys, `CHECK` constraints, and cascades
See [Database](website/database.md) for the schema and the app's current SQL See [Database](website/database.md) for the schema and the app's SQL
error-handling approach in more detail. error-handling approach in more detail.
## Frontend architecture ## Frontend architecture
### Active website (`web/frontend`) ### Website (`web/frontend`)
The current website is a React Router 7 application with server-side rendering The website is a React Router 7 application with server-side rendering
enabled. enabled.
```text ```text
@@ -397,7 +409,7 @@ web/frontend/
### Theme system ### Theme system
The active website uses semantic DaisyUI theme tokens backed by four Biergarten The website uses semantic DaisyUI theme tokens backed by four Biergarten
themes: themes:
- Biergarten Lager - Biergarten Lager
@@ -408,11 +420,10 @@ themes:
All component styling should prefer semantic tokens such as `primary`, All component styling should prefer semantic tokens such as `primary`,
`success`, `surface`, and `highlight` instead of hard-coded color values. `success`, `surface`, and `highlight` instead of hard-coded color values.
### Legacy frontend ### Archived frontend
The previous Next.js frontend has been archived at `archive/next-js-web-app/` `archive/next-js-web-app/` contains an archived Next.js frontend, kept for
for reference only. Active product and engineering documentation should point to reference. Product and engineering documentation points to `web/frontend`.
`web/frontend`.
## Security architecture ## Security architecture
@@ -431,8 +442,8 @@ for reference only. Active product and engineering documentation should point to
3. **Login**: 3. **Login**:
- User submits credentials - User submits credentials
- Password verified against hash - Password verified against hash
- Access and refresh JWTs are issued; the refresh token is stored server-side - Access and refresh JWTs are issued and returned to the client; neither is
against the user's credential row stored server-side
4. **Token refresh**: 4. **Token refresh**:
- Client exchanges a valid, unexpired refresh token for a new access/refresh - Client exchanges a valid, unexpired refresh token for a new access/refresh

View File

@@ -47,7 +47,7 @@ as a source of truth for brewing techniques or cultural representation.**
**Natural language processing is a powerful tool for data analysis and **Natural language processing is a powerful tool for data analysis and
generation, but its output should be treated with scrutiny. Human language is generation, but its output should be treated with scrutiny. Human language is
not simply data points to be analyzed — it carries cultural and human meaning not simply data points to be analyzed: it carries cultural and human meaning
that artificial intelligence cannot capture.** that artificial intelligence cannot capture.**
--- ---
@@ -102,7 +102,7 @@ vendored verbatim, unmodified, from
(the `common-forenames-by-country.json` / `common-surnames-by-country.json` (the `common-forenames-by-country.json` / `common-surnames-by-country.json`
release assets), released under **CC0** (public domain). That dataset's own release assets), released under **CC0** (public domain). That dataset's own
forename/surname lists are pulled from Wikipedia's "Lists of most common forename/surname lists are pulled from Wikipedia's "Lists of most common
surnames" and "List of most popular given names" as of the week of 2023-07-08 — surnames" and "List of most popular given names" as of the week of 2023-07-08;
see that project's README for full provenance. Names are not LLM-generated; this see that project's README for full provenance. Names are not LLM-generated; this
is curated fixture data per ROADMAP.md §2. Per-forename gender from the source is curated fixture data per ROADMAP.md §2. Per-forename gender from the source
data is preserved through to the sampled `Name` (rather than discarded during data is preserved through to the sampled `Name` (rather than discarded during
@@ -113,7 +113,7 @@ for surnames) rather than trimmed to `locations.json`'s current country list, so
it doesn't need re-sourcing if more countries are added later. `SampleName()` (a it doesn't need re-sourcing if more countries are added later. `SampleName()` (a
free helper in `generate_users.cc`) returns no result for a country present in free helper in `generate_users.cc`) returns no result for a country present in
neither file; of the countries in `locations.json`, that's currently `KE`, `SE`, neither file; of the countries in `locations.json`, that's currently `KE`, `SE`,
`SG`, `TH`, `VN`, and `ZA` — `GenerateUsers` skips cities in those countries the `SG`, `TH`, `VN`, and `ZA`; `GenerateUsers` skips cities in those countries the
same way brewery generation skips cities whose enrichment lookup fails. same way brewery generation skips cities whose enrichment lookup fails.
--- ---
@@ -290,8 +290,8 @@ 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 uniformly at random within a 5 km disc centred on the city's curated
`latitude`/`longitude` from `locations.json`. `latitude`/`longitude` from `locations.json`.
The resulting point is **plausible by construction** — it always falls within 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. 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 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 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 dataset: fixture data for a proof-of-concept, not a source of truth.

View File

@@ -7,12 +7,12 @@ deterministic mock.
> **This pipeline produces AI-generated data.** It is not a source of truth for > **This pipeline produces AI-generated data.** It is not a source of truth for
> brewing techniques, cultural representation, or local-language accuracy. See > brewing techniques, cultural representation, or local-language accuracy. See
> [ETHICS-AND-KNOWN-ISSUES.md](./ETHICS-AND-KNOWN-ISSUES.md) for a full > [ETHICS-AND-KNOWN-ISSUES.md](./ETHICS-AND-KNOWN-ISSUES.md) for full
> documentation of limitations, hallucination patterns, and bias. > documentation of limitations, hallucination patterns, and bias.
--- ---
## Table of Contents ## Table of contents
- [How it fits the main app](#how-it-fits-the-main-app) - [How it fits the main app](#how-it-fits-the-main-app)
- [Quick start](#quick-start) - [Quick start](#quick-start)
@@ -176,7 +176,7 @@ output to confirm the fast path was taken.
### Run the container ### Run the container
The container always runs the model-backed path; there is no `--mocked` The container always runs the model-backed path; there is no `--mocked`
container mode (use a native build for that — see [Quick start](#quick-start)). container mode (use a native build for that; see [Quick start](#quick-start)).
The entrypoint, `runpod/start.sh`, downloads the GGUF model automatically if it The entrypoint, `runpod/start.sh`, downloads the GGUF model automatically if it
is not already present at the configured path. is not already present at the configured path.
@@ -191,7 +191,7 @@ docker run --rm \
By default this downloads `google_gemma-4-E4B-it-Q6_K.gguf` to `./models/` on By default this downloads `google_gemma-4-E4B-it-Q6_K.gguf` to `./models/` on
first run if it isn't already there. To use a pre-downloaded model, place it at first run if it isn't already there. To use a pre-downloaded model, place it at
that path first — see [Model](#model) above. that path first; see [Model](#model).
#### Environment variables #### Environment variables
@@ -211,8 +211,9 @@ configurable via environment variable.
Use a GPU pod template. Mount persistent storage for `/workspace/models`, Use a GPU pod template. Mount persistent storage for `/workspace/models`,
`/workspace/output`, and `/workspace/logs`. See `/workspace/output`, and `/workspace/logs`. See
`tooling/pipeline/runpod/pod-template.yaml` for a starter template — set the `tooling/pipeline/runpod/pod-template.yaml` for a starter template; set the
environment variables listed above to match your run. environment variables listed in [Environment variables](#environment-variables)
to match your run.
--- ---
@@ -236,24 +237,24 @@ skipped and the pipeline continues. `GenerateUsers()` runs before
### Key components ### Key components
- `src/main.cc` — argument parsing and Boost.DI composition root. - `src/main.cc`: argument parsing and Boost.DI composition root.
- `CuratedJsonDataService` — implements `ICuratedDataService`; takes a - `CuratedJsonDataService`: implements `ICuratedDataService`; takes a
`CuratedDataFilePaths` DTO (locations/personas/forenames/surnames paths) in `CuratedDataFilePaths` DTO (locations/personas/forenames/surnames paths) in
its constructor, then parses and validates curated location, persona, and its constructor, then parses and validates curated location, persona, and
forename/surname JSON, memoizing each result after its first load on a given forename/surname JSON, memoizing each result after its first load on a given
instance. `MockCuratedDataService` is the in-memory substitute (4 fixed instance. `MockCuratedDataService` is the in-memory substitute (4 fixed
locations, 3 personas, and name data for `US`/`DE`/`FR`/`BE`) used in locations, 3 personas, and name data for `US`/`DE`/`FR`/`BE`) used in
`--mocked` runs. `--mocked` runs.
- `WikipediaEnrichmentService` — queries Wikipedia extracts, caches results, - `WikipediaEnrichmentService`: queries Wikipedia extracts, caches results,
returns empty context on failure. `MockEnrichmentService` is the no-op returns empty context on failure. `MockEnrichmentService` is the no-op
substitute used in `--mocked` runs. substitute used in `--mocked` runs.
- `LlamaGenerator` — formats prompts for Gemma 4, validates JSON output for both - `LlamaGenerator`: formats prompts for Gemma 4, validates JSON output for both
`GenerateBrewery` and `GenerateUser`, retries malformed responses up to three `GenerateBrewery` and `GenerateUser`, retries malformed responses up to three
times with corrective feedback in the retry prompt. The token budget is fixed times with corrective feedback in the retry prompt. The token budget is fixed
across attempts; it is not raised automatically on truncation. across attempts; it is not raised automatically on truncation.
- `MockGenerator` — stable hash-based output so the same city/persona/name input - `MockGenerator`: stable hash-based output so the same city/persona/name input
always produces the same brewery or user. always produces the same brewery or user.
- `SqliteExportService` — creates a dated SQLite file per run and persists each - `SqliteExportService`: creates a dated SQLite file per run and persists each
successful user and brewery into normalized tables. successful user and brewery into normalized tables.
- Brewery payloads include English and local-language name and description - Brewery payloads include English and local-language name and description
fields. User payloads carry a sampled first/last name and gender, an fields. User payloads carry a sampled first/last name and gender, an
@@ -281,13 +282,13 @@ runs.
`CuratedJsonDataService` memoizes each of `LoadLocations()`, `LoadPersonas()`, `CuratedJsonDataService` memoizes each of `LoadLocations()`, `LoadPersonas()`,
`LoadForenamesByCountry()`, and `LoadSurnamesByCountry()` independently the `LoadForenamesByCountry()`, and `LoadSurnamesByCountry()` independently the
first time each is called, since `BiergartenPipelineOrchestrator` owns a single first time each is called, since `BiergartenPipelineOrchestrator` owns a single
`ICuratedDataService` instance for the whole run — later calls return the cached `ICuratedDataService` instance for the whole run; later calls return the cached
result instead of re-parsing. result instead of re-parsing.
`GenerateUsers()` samples a forename/surname pair per city via `SampleName()`, `GenerateUsers()` samples a forename/surname pair per city via `SampleName()`,
keyed by the city's ISO 3166-1 code. Countries present in `locations.json` but keyed by the city's ISO 3166-1 code. Countries present in `locations.json` but
absent from either name fixture (currently `KE`, `SE`, `SG`, `TH`, `VN`, `ZA`) absent from either name fixture (currently `KE`, `SE`, `SG`, `TH`, `VN`, `ZA`)
are skipped, the same way a failed enrichment or generation call skips a city — are skipped, the same way a failed enrichment or generation call skips a city;
see ETHICS-AND-KNOWN-ISSUES.md's Names-by-Country Dataset section. see ETHICS-AND-KNOWN-ISSUES.md's Names-by-Country Dataset section.
### Process flow - activity diagram ### Process flow - activity diagram
@@ -357,15 +358,14 @@ The build fetches Boost.DI, spdlog, and SQLite via CMake. llama.cpp is fetched
only when a system installation is not detected. Metal is enabled on Apple only when a system installation is not detected. Metal is enabled on Apple
Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present. Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
> **Code Style:** Code style generally follows the Google Style Guide and > **Code style:** The code follows the Google C++ Style Guide, except that
> features modern C++20 throughout. Formatting follows the Google C++ Style > indentation is 3 spaces instead of Google's 2, and uses C++20 throughout.
> Guide, with the exception of indentation width (3 spaces vs. Google's 2).
--- ---
## Tested hardware ## Tested hardware
### ARM macOS — M1 Pro ### ARM macOS (M1 Pro)
| | | | | |
| --------- | --------------------------------- | | --------- | --------------------------------- |
@@ -376,7 +376,7 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
| Model | Gemma 4 E4B | | Model | Gemma 4 E4B |
| Inference | llama.cpp with Metal | | Inference | llama.cpp with Metal |
### x86_64 Linux — NVIDIA RTX 2000 ### x86_64 Linux (NVIDIA RTX 2000)
| | | | | |
| --------- | ------------------------------ | | --------- | ------------------------------ |
@@ -387,7 +387,7 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
| Model | Gemma 4 E4B | | Model | Gemma 4 E4B |
| Inference | llama.cpp with CUDA 12.x | | Inference | llama.cpp with CUDA 12.x |
### x86_64 Linux — Docker / RunPod (NVIDIA CUDA) ### x86_64 Linux (Docker / RunPod, NVIDIA CUDA)
| | | | | |
| --------- | ------------------------------------------- | | --------- | ------------------------------------------- |
@@ -407,7 +407,7 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
- Keep `locations.json` structured enough to support discovery and future - Keep `locations.json` structured enough to support discovery and future
filtering. filtering.
- `personas.json`, `forenames-by-country.json`, and `surnames-by-country.json` - `personas.json`, `forenames-by-country.json`, and `surnames-by-country.json`
are curated/vendored fixture data, not LLM-generated — see are curated/vendored fixture data, not LLM-generated; see
ETHICS-AND-KNOWN-ISSUES.md's Names-by-Country Dataset section for provenance. ETHICS-AND-KNOWN-ISSUES.md's Names-by-Country Dataset section for provenance.
- Treat SQLite output as seed material for the app's brewery and user domains, - Treat SQLite output as seed material for the app's brewery and user domains,
not production data. not production data.
@@ -435,18 +435,18 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
Paths below are relative to `tooling/pipeline/`. Paths below are relative to `tooling/pipeline/`.
- `src/main.cc` — argument parsing and DI composition root. - `src/main.cc`: argument parsing and DI composition root.
- `src/biergarten_pipeline_orchestrator/` — orchestration, sampling, logging, - `src/biergarten_pipeline_orchestrator/`: orchestration, sampling, logging,
and export. and export.
- `src/services/curated_data/` — `CuratedJsonDataService`, the file-backed - `src/services/curated_data/`: `CuratedJsonDataService`, the file-backed
`ICuratedDataService`, and `MockCuratedDataService`, the in-memory `ICuratedDataService`, and `MockCuratedDataService`, the in-memory
`ICuratedDataService` used in `--mocked` runs. `ICuratedDataService` used in `--mocked` runs.
- `src/services/enrichment/wikipedia/` — enrichment service and cache. - `src/services/enrichment/wikipedia/`: enrichment service and cache.
- `src/services/sqlite/` — SQLite export implementation. - `src/services/sqlite/`: SQLite export implementation.
- `src/data_generation/llama/` — local inference, prompt loading, output - `src/data_generation/llama/`: local inference, prompt loading, output
validation. validation.
- `src/data_generation/mock/` — deterministic fallback. - `src/data_generation/mock/`: deterministic fallback.
- `runpod/` — container build and runtime launcher. - `runpod/`: container build and runtime launcher.
--- ---
@@ -458,7 +458,7 @@ exercise the full brewery and social domains without live data. For the detailed
engineering breakdown of what's needed to reach the architecture in engineering breakdown of what's needed to reach the architecture in
[`diagrams/planned/`](./diagrams/planned/), see [ROADMAP.md](./ROADMAP.md). [`diagrams/planned/`](./diagrams/planned/), see [ROADMAP.md](./ROADMAP.md).
### Testing — very high priority ### Testing (very high priority)
- Unit test JSON validation and retry logic against malformed, truncated, and - Unit test JSON validation and retry logic against malformed, truncated, and
empty model outputs. empty model outputs.
@@ -481,7 +481,7 @@ enough to exercise search, sort, and category filters.
### Check-in system ### Check-in system
Produce timestamped check-in events between users and breweries. Use a J-curve Produce timestamped check-in events between users and breweries. Use a J-curve
activity profile — a small set of users accounts for most check-ins, the rest activity profile: a small set of users accounts for most check-ins, the rest
appear occasionally. Add bursty behaviour around weekends and travel periods. appear occasionally. Add bursty behaviour around weekends and travel periods.
### Beer ratings ### Beer ratings

View File

@@ -59,8 +59,8 @@ 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 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 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 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 editing editing an existing one in place: DbUp's tracking is per-script, so an edited
an already-applied script means it won't re-run against existing databases. already-applied script never re-runs against existing databases.
## Data seeding ## Data seeding
@@ -68,7 +68,7 @@ an already-applied script means it won't re-run against existing databases.
pipeline (see [Pipeline README](../pipeline/README.md)): countries, pipeline (see [Pipeline README](../pipeline/README.md)): countries,
state/provinces, and cities (created on demand while resolving each brewery's state/provinces, and cities (created on demand while resolving each brewery's
location), user accounts, and brewery posts with locations. It doesn't call location), user accounts, and brewery posts with locations. It doesn't call
every feature slice that has a write path — `Photo`, for example, has a every feature slice that has a write path: `Photo`, for example, has a
repository (`Features.PhotoUpload`) but no seed data. repository (`Features.PhotoUpload`) but no seed data.
## Database error handling ## Database error handling
@@ -78,10 +78,10 @@ repository (`Features.PhotoUpload`) but no seed data.
- On failure, the repository throws a `Domain.Exceptions` type directly - On failure, the repository throws a `Domain.Exceptions` type directly
(`NotFoundException`, `ConflictException`, `ArgumentException`, ...). (`NotFoundException`, `ConflictException`, `ArgumentException`, ...).
`API.Core`'s `GlobalExceptionFilter` maps these to HTTP status codes (404, `API.Core`'s `GlobalExceptionFilter` maps these to HTTP status codes (404,
409, 400, ...); an unmapped `SqlException` (e.g. a genuine connectivity or 409, 400, ...); an unmapped `SqlException` (for example, a genuine
constraint-violation failure) maps to 503. connectivity or constraint-violation failure) maps to 503.
- One exception: `Features.Users`' `DapperUserStore` does catch - One exception: `Features.Users`' `DapperUserStore` does catch
`SqlException` directly, to detect a duplicate-key race (SQL Server error `SqlException` directly, to detect a duplicate-key race (SQL Server error
2601 or 2627) when two requests try to verify the same account 2601 or 2627) when two requests try to verify the same account
concurrently — see `DapperUserStore.IsDuplicateKeyViolation`. concurrently; see `DapperUserStore.IsDuplicateKeyViolation`.

View File

@@ -100,7 +100,8 @@ frontend.tests # Storybook Vitest + Playwright suites (own Playwright image)
```bash ```bash
# Run all tests # Run all tests
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up --abort-on-container-exit docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up -d
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml wait api.specs unit.tests frontend.tests
# View results # View results
ls -la test-results/ ls -la test-results/
@@ -111,6 +112,13 @@ cat test-results/Features.Users.Tests.trx
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v 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
[Testing](testing.md#running-tests-with-docker-recommended) for details.
### 3. Production (`docker-compose.prod.yaml`) ### 3. Production (`docker-compose.prod.yaml`)
**Purpose**: Production-ready deployment **Purpose**: Production-ready deployment

View File

@@ -1,7 +1,6 @@
# Environment variables # Environment variables
This document covers the active environment variables used by the current This document covers the environment variables used by the Biergarten stack.
Biergarten stack.
## Overview ## Overview
@@ -19,10 +18,10 @@ Direct environment variable access via `Environment.GetEnvironmentVariable()`.
### Frontend (`web/frontend`) ### Frontend (`web/frontend`)
The active website reads runtime values from the server environment for its auth The website reads runtime values from the server environment for its auth and
and API integration. In Docker, the `frontend` service is wired into the same API integration. In Docker, the `frontend` service is wired into the same
`web/.env.dev` / `.env.test` / `.env.prod` files as the backend; running it `web/.env.dev` / `.env.test` / `.env.prod` files as the backend; running it
locally with `npm run dev` still reads from the shell environment. locally with `npm run dev` reads from the shell environment.
### Docker ### Docker
@@ -104,7 +103,8 @@ WEBSITE_BASE_URL=https://thebiergarten.app # Base URL for the website
**Security Requirements**: **Security Requirements**:
- Use at least 32 characters per secret - Use at least 32 characters per secret
- Use 127 or more characters in production - Use secrets generated from 127 or more random bytes in production (base64
encoding turns 127 bytes into a longer string, well over 127 characters)
- Generate secrets with a cryptographically secure random function - Generate secrets with a cryptographically secure random function
- Never reuse secrets across token types or environments - Never reuse secrets across token types or environments
- Rotate secrets periodically in production - Rotate secrets periodically in production
@@ -170,10 +170,17 @@ ASPNETCORE_URLS=http://0.0.0.0:8080 # Binding address and port
DOTNET_RUNNING_IN_CONTAINER=true # Flag for container execution 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.
## Frontend variables (`web/frontend`) ## Frontend variables (`web/frontend`)
The active website does not use the old Next.js/Prisma environment model. Its The website's core runtime variables are:
core runtime variables are:
```bash ```bash
API_BASE_URL=http://api.core:8080 # Base URL for the .NET API (container DNS name in Docker) API_BASE_URL=http://api.core:8080 # Base URL for the .NET API (container DNS name in Docker)
@@ -291,12 +298,6 @@ cp web/.env.example web/.env.dev
# Edit web/.env.dev with your values # Edit web/.env.dev with your values
``` ```
## Legacy frontend variables
Variables for the archived Next.js frontend (`archive/next-js-web-app/`) have
been removed from this active reference, since that app is retained for
reference only and is not run as part of the active stack.
**Docker Compose Mapping**: **Docker Compose Mapping**:
- `web/docker-compose.dev.yaml` → `web/.env.dev` - `web/docker-compose.dev.yaml` → `web/.env.dev`
@@ -334,7 +335,7 @@ reference only and is not run as part of the active stack.
| `PORT` | | ✓ | ✓ | No | Frontend server port | | `PORT` | | ✓ | ✓ | No | Frontend server port |
| `CLEAR_DATABASE` | ✓ | | ✓ | No | Dev/test reset flag | | `CLEAR_DATABASE` | ✓ | | ✓ | No | Dev/test reset flag |
| `ASPNETCORE_ENVIRONMENT` | ✓ | | ✓ | Yes | ASP.NET environment | | `ASPNETCORE_ENVIRONMENT` | ✓ | | ✓ | Yes | ASP.NET environment |
| `ASPNETCORE_URLS` | ✓ | | ✓ | Yes | API binding address | | `ASPNETCORE_URLS` | ✓ | | ✓ | Yes\*\* | API binding address |
| `SA_PASSWORD` | | | ✓ | Yes | SQL Server container | | `SA_PASSWORD` | | | ✓ | Yes | SQL Server container |
| `ACCEPT_EULA` | | | ✓ | Yes | SQL Server EULA | | `ACCEPT_EULA` | | | ✓ | Yes | SQL Server EULA |
| `MSSQL_PID` | | | ✓ | No | SQL Server edition | | `MSSQL_PID` | | | ✓ | No | SQL Server edition |
@@ -343,6 +344,10 @@ reference only and is not run as part of the active stack.
\* Either `DB_CONNECTION_STRING` OR the component variables (`DB_SERVER`, \* Either `DB_CONNECTION_STRING` OR the component variables (`DB_SERVER`,
`DB_NAME`, `DB_USER`, `DB_PASSWORD`) must be provided. `DB_NAME`, `DB_USER`, `DB_PASSWORD`) must be provided.
\*\* Required for Docker, where it's hardcoded in the compose files. Outside
Docker, `dotnet run` falls back to the `applicationUrl` in
`launchSettings.json`.
## Validation ## Validation
### Backend validation ### Backend validation
@@ -359,8 +364,8 @@ Variables are validated at startup:
### Frontend validation ### Frontend validation
The active website relies on runtime defaults for local development and the The website relies on runtime defaults for local development and on the server
surrounding server environment in deployed environments. environment when deployed.
- `API_BASE_URL` defaults to `http://localhost:8080` - `API_BASE_URL` defaults to `http://localhost:8080`
- `SESSION_SECRET` falls back to a development-only local secret - `SESSION_SECRET` falls back to a development-only local secret

View File

@@ -1,7 +1,7 @@
# Getting started # Getting started
This guide covers local setup for the current Biergarten stack: the .NET backend This guide covers local setup for the Biergarten stack: the .NET backend in
in `web/backend` and the active React Router frontend in `web/frontend`. `web/backend` and the React Router frontend in `web/frontend`.
## Prerequisites ## Prerequisites
@@ -69,7 +69,7 @@ Available endpoints:
- Mailpit UI: http://localhost:8025 - Mailpit UI: http://localhost:8025
- SeaweedFS S3 API: http://localhost:8333 - SeaweedFS S3 API: http://localhost:8333
### 4. Start the active frontend ### 4. Start the frontend
```bash ```bash
cd web/frontend cd web/frontend
@@ -146,10 +146,10 @@ dotnet run --project Database/Database.Seed/Database.Seed.csproj
dotnet run --project API/API.Core/API.Core.csproj dotnet run --project API/API.Core/API.Core.csproj
``` ```
## Legacy frontend note ## Archived frontend
The previous Next.js frontend lives in `archive/next-js-web-app/` for reference `archive/next-js-web-app/` contains an archived Next.js frontend, kept for
only and is not the active website. reference; it is not part of the running stack.
## Next steps ## Next steps

View File

@@ -21,8 +21,8 @@ The project uses a multi-layered testing approach across backend and frontend:
## Running tests with Docker (recommended) ## Running tests with Docker (recommended)
The easiest way to run all tests is using Docker Compose, which sets up an To run all tests, use Docker Compose, which sets up an isolated test
isolated test environment: environment:
```bash ```bash
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up -d docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up -d
@@ -43,7 +43,7 @@ This command:
stopped, regardless of which one finishes first. Long-running services stopped, regardless of which one finishes first. Long-running services
(`sqlserver`, `seaweedfs`) and the one-shot `database.migrations` / (`sqlserver`, `seaweedfs`) and the one-shot `database.migrations` /
`database.seed` jobs keep running in the background alongside them and are `database.seed` jobs keep running in the background alongside them and are
torn down separately (see below) — `wait` doesn't touch them. torn down separately (see below); `wait` doesn't touch them.
### View test results ### View test results
@@ -355,15 +355,12 @@ The workflow inspects the exit code of each test container
after `wait` returns and fails the job if any of them is non-zero. 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-results/` is uploaded as a build artifact regardless of outcome.
Earlier versions of this workflow used `up -d` + `wait` keeps container teardown independent of any single
`up --abort-on-container-exit`, which stops the entire stack the instant container's exit. The one-shot `database.migrations`/`database.seed` jobs
*any* watched container exits. That's wrong here: the one-shot exit 0 partway through the run, and `frontend.tests` (which has no database
`database.migrations`/`database.seed` jobs are expected to exit 0 partway dependency) reliably finishes before `api.specs`/`unit.tests` start; because
through, and even scoping the flag to just the three test services doesn't `wait` blocks on all three test containers, neither early exit ends the run
help, since `frontend.tests` (no database dependency) reliably finishes prematurely.
before `api.specs`/`unit.tests` even start, aborting them prematurely.
`up -d` + `wait` avoids both races by not tying container teardown to any
single container's exit.
Exit codes: Exit codes:
@@ -414,13 +411,13 @@ act push \
`sqlserver` service's image is amd64-only, and act needs to know to `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 emulate that platform for the whole job container, not just that one
service. service.
- No secrets or `.actrc` are needed — `generate-env.sh` creates its own - No secrets or `.actrc` are needed: `generate-env.sh` creates its own
`.env.test` with freshly randomized values, same as in real CI. `.env.test` with freshly randomized values, same as in real CI.
**Known act-only limitation**: the "Upload test results" step **Known act-only limitation**: the "Upload test results" step
(`actions/upload-artifact@v4`) fails locally with `Unable to get the (`actions/upload-artifact@v4`) fails locally with `Unable to get the
ACTIONS_RUNTIME_TOKEN env variable`. act doesn't provide a real Actions 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 artifact backend, so this step, and only this step, is expected to fail
under act even when everything else passes. It works normally on under act even when everything else passes. It works normally on
GitHub-hosted runners. GitHub-hosted runners.
@@ -429,7 +426,7 @@ names and named volumes (`sqlserverdata-test`, `seaweedfsdata-test`) scoped
to the `web` compose project. If a previous local run (via `act` or a 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 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 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 `.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. 'sa'` errors that look like a test bug but are really a leftover volume.
Clear it before re-running: Clear it before re-running:

View File

@@ -34,8 +34,7 @@ Low-level JWT operations.
#### [ITokenService](../../web/backend/Features/Features.Users/Services/ITokenService.cs) #### [ITokenService](../../web/backend/Features/Features.Users/Services/ITokenService.cs)
Both token generation and validation live on the same slice-internal service Token generation and validation live on the same slice-internal service.
(there is no separate validation service/class).
**Generation methods:** **Generation methods:**
@@ -79,9 +78,17 @@ directly.
1. Receives confirmation token from user via `ConfirmUserCommand` 1. Receives confirmation token from user via `ConfirmUserCommand`
2. Calls `ITokenService.ValidateConfirmationTokenAsync()` 2. Calls `ITokenService.ValidateConfirmationTokenAsync()`
3. Extracts user ID from validated token and looks it up via `UserManager<ApplicationUser>` 3. Extracts user ID from validated token and looks it up via `UserManager<ApplicationUser>`
4. Marks the account confirmed via `IUserEmailStore<ApplicationUser>.SetEmailConfirmedAsync()` 4. Marks the account confirmed via `IUserEmailStore<ApplicationUser>.SetEmailConfirmedAsync()`,
then persists it via `UserManager<ApplicationUser>.UpdateAsync()`
5. Returns confirmation result 5. Returns confirmation result
`UserManager<ApplicationUser>` and `IUserEmailStore<ApplicationUser>` are
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.
#### [ResendConfirmationEmailHandler](../../web/backend/Features/Features.Users/Commands/Authentication/ResendConfirmationEmail/ResendConfirmationEmailHandler.cs) #### [ResendConfirmationEmailHandler](../../web/backend/Features/Features.Users/Commands/Authentication/ResendConfirmationEmail/ResendConfirmationEmailHandler.cs)
**Flow:** **Flow:**