Documentation updates (#236)

Remove vague or flowery language, use Simplified Technical English whenever possible
This commit is contained in:
Aaron Po
2026-08-04 09:38:24 -04:00
parent 30cdb48491
commit b27b5bdb40
10 changed files with 272 additions and 250 deletions

View File

@@ -2,7 +2,7 @@
This document describes the active 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
active website:
@@ -29,9 +29,9 @@ For visual representations, see:
- [database-schema.svg](website/diagrams-out/database-schema.svg) - Database
relationships
## Backend Architecture
## Backend architecture
### Vertical Slice Architecture Pattern
### Vertical-slice architecture pattern
The backend organizes business capabilities as feature slices instead of
technical layers. Each feature (`Features.Auth`, `Features.Breweries`,
@@ -80,9 +80,9 @@ interaction (`Features.Auth` triggering a confirmation email handled by
whose contract lives in `Shared.Application`, so neither slice takes a
project reference on the other.
### Layer Responsibilities
### Layer responsibilities
#### API Layer (`API.Core`)
#### API layer (`API.Core`)
**Purpose**: Thin ASP.NET Core host: no business logic, no controllers of
its own
@@ -108,7 +108,7 @@ its own
- No controllers, no business logic, no feature-specific contracts
- Exists purely to host and wire up the feature slices
#### Feature Slices (`Features.Auth`, `Features.Breweries`, `Features.UserManagement`, `Features.Emails`)
#### Feature slices (`Features.Auth`, `Features.Breweries`, `Features.UserManagement`, `Features.Emails`)
**Purpose**: Each slice is the complete vertical for one business capability
@@ -146,7 +146,7 @@ MediatR commands sent from other slices, never over HTTP.
MediatR
- Read endpoints return a dedicated `Dto`, never the domain entity directly
#### Shared Projects (`Shared.Contracts`, `Shared.Application`)
#### Shared projects (`Shared.Contracts`, `Shared.Application`)
**Purpose**: The minimum cross-slice surface area required because every
slice needs it, or because duplicating it four times would be worse than
@@ -166,7 +166,7 @@ sharing it
- Kept deliberately small: this is the exception to "no slice depends on
another slice," not a general-purpose dumping ground
#### Infrastructure Layer
#### Infrastructure layer
**Purpose**: Technical capabilities and external integrations, shared by
whichever slices need them
@@ -185,14 +185,14 @@ whichever slices need them
**Dependencies**:
- Domain entities
- External libraries (ADO.NET, JWT, Argon2, MailKit, etc.)
- External libraries (ADO.NET, JWT, Argon2, MailKit, and so on)
**Rules**:
- Implements technical concerns only, no business logic
- Reusable across slices
#### Domain Layer (`Domain.Entities`)
#### Domain layer (`Domain.Entities`)
**Purpose**: Core business entities and models
@@ -213,9 +213,9 @@ whichever slices need them
- No infrastructure references
- Represents business concepts
### Design Patterns
### Design patterns
#### Vertical Slice + MediatR
#### Vertical slice + MediatR
**Purpose**: Organize code by feature instead of by technical layer, so
everything needed to understand or change one capability lives in one
@@ -223,9 +223,9 @@ project
**Implementation**:
- Each HTTP write operation is a `Command` (e.g. `CreateBreweryCommand` in
`Features.Breweries/Commands/CreateBrewery/`); each read operation is a
`Query` (e.g. `GetBreweryByIdQuery`)
- Each HTTP write operation is a `Command` (for example, `CreateBreweryCommand`
in `Features.Breweries/Commands/CreateBrewery/`); each read operation is a
`Query` (for example, `GetBreweryByIdQuery`)
- Controllers bind directly to the Command/Query as the request body;
there is no separate request DTO + mapping step for writes
- A single shared `ValidationBehavior<TRequest,TResponse>`
@@ -246,7 +246,7 @@ public class CreateBreweryHandler(IBreweryRepository repository) : IRequestHandl
}
```
#### Repository Pattern
#### Repository pattern
**Purpose**: Abstract database access behind interfaces
@@ -275,7 +275,7 @@ public interface IAuthRepository
}
```
#### Dependency Injection
#### Dependency injection
**Purpose**: Loose coupling and testability
@@ -289,7 +289,7 @@ extension method that registers its repository and slice-internal services
- Singleton: `ISqlConnectionFactory`
- Transient: Utilities, helpers
#### SQL-First Approach
#### SQL-first approach
**Purpose**: Push complex logic into the database
@@ -306,9 +306,9 @@ extension method that registers its repository and slice-internal services
- `USP_GetUserAccountByUsername` - User lookup
- `USP_RotateUserCredential` - Password update
## Frontend Architecture
## Frontend architecture
### Active Website (`web/frontend`)
### Active website (`web/frontend`)
The current website is a React Router 7 application with server-side rendering
enabled.
@@ -327,7 +327,7 @@ web/frontend/
└── package.json Frontend scripts and dependencies
```
### Frontend Responsibilities
### Frontend responsibilities
- Render the auth demo and theme guide routes
- Manage cookie-backed website session state
@@ -335,7 +335,7 @@ web/frontend/
- Provide shared UI building blocks for forms, navigation, themes, and toasts
- Supply Storybook documentation and browser-based component verification
### Theme System
### Theme system
The active website uses semantic DaisyUI theme tokens backed by four Biergarten
themes:
@@ -348,15 +348,15 @@ themes:
All component styling should prefer semantic tokens such as `primary`,
`success`, `surface`, and `highlight` instead of hard-coded color values.
### Legacy Frontend
### Legacy frontend
The previous Next.js frontend has been archived at `archive/next-js-web-app/`
for reference only. Active product and engineering documentation should point
to `web/frontend`.
## Security Architecture
## Security architecture
### Authentication Flow
### Authentication flow
1. **Registration**:
- User submits credentials
@@ -375,7 +375,7 @@ to `web/frontend`.
- Middleware validates token
- Request proceeds if valid
### Password Security
### Password security
**Algorithm**: Argon2id
@@ -385,7 +385,7 @@ to `web/frontend`.
- Salt: 128-bit (16 bytes)
- Hash: 256-bit (32 bytes)
### JWT Tokens
### JWT tokens
**Algorithm**: HS256 (HMAC-SHA256)
@@ -409,9 +409,9 @@ to `web/frontend`.
}
```
## Database Architecture
## Database architecture
### SQL-First Philosophy
### SQL-first philosophy
**Principles**:
@@ -427,7 +427,7 @@ to `web/frontend`.
- Version-controlled schema (migrations)
- Easier query profiling and tuning
### Migration Strategy
### Migration strategy
**Tool**: DbUp
@@ -448,7 +448,7 @@ scripts/
└── ...
```
### Data Seeding
### Data seeding
**Purpose**: Populate development/test databases
@@ -460,9 +460,9 @@ scripts/
- Test user accounts
- Sample breweries (future)
## Deployment Architecture
## Deployment architecture
### Docker Containerization
### Docker containerization
**Container Structure**:
@@ -479,10 +479,10 @@ scripts/
For details, see [Docker Guide](website/docker.md).
### Health Checks
### Health checks
**SQL Server**: Validates database connectivity **API**: Checks service health
and dependencies
- **SQL Server**: Validates database connectivity
- **API**: Checks service health and dependencies
**Configuration**:
@@ -494,9 +494,9 @@ healthcheck:
start_period: 30s
```
## Testing Architecture
## Testing architecture
### Test Pyramid
### Test pyramid
```
┌──────────────┐

View File

@@ -1,28 +1,28 @@
# Ethics, Bias, and Known Issues
# Ethics, bias, and known issues
This document covers the ethical context of the Biergarten Pipeline's output,
the model's biases, and known issues including hallucinated brewing science and
low-resource language failures.
> Note that all testing was used using `google_gemma-4-E4B-it-Q6_K.gguf`.
> All testing described below used `google_gemma-4-E4B-it-Q6_K.gguf`.
## Table of Contents
## Table of contents
- [What This Dataset Is](#what-this-dataset-is)
- [What This Dataset Is Not](#what-this-dataset-is-not)
- [Model Bias and Language Quality](#model-bias-and-language-quality)
- [Western and Eurocentric Lens](#western-and-eurocentric-lens)
- [Wikipedia Enrichment](#wikipedia-enrichment)
- [Names-by-Country Dataset](#names-by-country-dataset)
- [The "Avoid AI Phrases" Prompt Instruction](#the-avoid-ai-phrases-prompt-instruction)
- [Known Issues](#known-issues)
- [Hallucinated Brewing Techniques](#hallucinated-brewing-techniques)
- [Low-Resource Language Hallucination](#low-resource-language-hallucination)
- [Synthetic Postal Codes](#synthetic-postal-codes)
- [What this dataset is](#what-this-dataset-is)
- [What this dataset is not](#what-this-dataset-is-not)
- [Model bias and language quality](#model-bias-and-language-quality)
- [Western and Eurocentric lens](#western-and-eurocentric-lens)
- [Wikipedia enrichment](#wikipedia-enrichment)
- [Names-by-country dataset](#names-by-country-dataset)
- [The "avoid AI phrases" prompt instruction](#the-avoid-ai-phrases-prompt-instruction)
- [Known issues](#known-issues)
- [Hallucinated brewing techniques](#hallucinated-brewing-techniques)
- [Low-resource language hallucination](#low-resource-language-hallucination)
- [Synthetic postal codes](#synthetic-postal-codes)
---
## What This Dataset Is
## What this dataset is
This is AI-generated fixture data for a proof-of-concept version of The
Biergarten App. Anyone who interacts with an application seeded from this
@@ -30,29 +30,29 @@ pipeline must be told upfront that the content is AI-generated.
---
## What This Dataset Is Not
## What this dataset is not
The pipeline is not intended to produce accurate brewing science, faithful
cultural representation, or reliable local-language text. Hallucinations such as
invented fermentation techniques, or incoherent local-language prose, are
expected, observed, and partially documented in [Known Issues](#known-issues)
expected, observed, and partially documented in [Known issues](#known-issues)
below.
Human control sits at the context layer (i.e. prompt design, Wikipedia
Human control sits at the context layer (that is, prompt design, Wikipedia
enrichment). Statistical output shapes in future pipeline stages (check-in
distributions, rating skews, activity profiles) will be handled the same way.
**Treat this data as an exercise in prompt engineering and model behaviour, not
as a source of truth for brewing techniques or cultural representation.**
**Natural language processing, although a powerful tool for data analysis and
generation is to be taken with scrutiny. Human language is not simply just data
points to be analyzed, but it also carries deep cultural and human meaning that
artificial intelligence is incapable of.**
**Natural language processing is a powerful tool for data analysis and
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
that artificial intelligence cannot capture.**
---
## Model Bias and Language Quality
## Model bias and language quality
The underlying model's training biases surface within this pipeline. Output
quality tracks with how well a language is represented in the training corpus:
@@ -67,9 +67,9 @@ large language models trained predominantly on English-language
material.[^llm-bias]
Mitigations are documented in
[Known Issues: Low-Resource Language Hallucination](#low-resource-language-hallucination).
[Known issues: Low-resource language hallucination](#low-resource-language-hallucination).
### Western and Eurocentric Lens
### Western and Eurocentric lens
The model's training data skews heavily Western and North American. When
generating brewery descriptions for Kinshasa, Abidjan, or Osaka, for example, it
@@ -81,19 +81,19 @@ some generation in city-specific material, but it does not eliminate the skew.
---
## Wikipedia Enrichment
## Wikipedia enrichment
City and beer context is fetched from the Wikipedia API. Wikipedia text is
co-licensed under the **Creative Commons Attribution-ShareAlike 4.0
International License (CC BY-SA 4.0)** and the **GNU Free Documentation License
(GFDL)**.[^wp-license]
International License (CC BY-SA 4.0)**[^cc-sa] and the **GNU Free Documentation
License (GFDL)**.[^wp-license]
Wikipedia's own accuracy limitations and editorial biases can propagate into
generated descriptions.
---
## Names-by-Country Dataset
## Names-by-country dataset
`tooling/pipeline/forenames-by-country.json` and `surnames-by-country.json`
(used to sample a `Name` per ISO 3166-1 country code for user generation) are
@@ -118,7 +118,7 @@ same way brewery generation skips cities whose enrichment lookup fails.
---
## The "Avoid AI Phrases" Prompt Instruction
## The "avoid AI phrases" prompt instruction
The system prompt instructs the model to avoid common AI-generated phrasing
patterns. This is a prompt engineering experiment:
@@ -130,14 +130,14 @@ consumers are informed of the AI-generated origin before engagement.
---
## Known Issues
## Known issues
### Hallucinated Brewing Techniques
### Hallucinated brewing techniques
When forced by the system prompt to generate a "highly specialized technical
brewing detail," the model frequently hallucinates fermentation science and
brewing chemistry. While the resulting sounds confident, it will be nonsensical
to reader with brewing and/or scientific expertise.
brewing chemistry. While the resulting text sounds confident, it is
nonsensical to a reader with brewing or scientific expertise.
Small-parameter models such as Gemma 4 E4B cannot reliably satisfy prompts
calling for specialist brewing detail. This is consistent with the **CHOKE**
@@ -155,9 +155,9 @@ and factually wrong output.
[2026-04-21 15:04:40.258] [info] brewery_description_local="早朝の、たこ焼きや香ばしいイカ焼きの湿った匂いは、いつも乾燥小屋の外にある使用済み麦芽の金属的な匂いと奇妙に混ざり合って近隣に漂います。私たちはこの場所に店を構えることを決めたのです。なぜなら、大阪は決して止まることのない都市であり、商品と味が絶え間なく交換されることで築かれた場所だからです。地元の多くの方々は、信頼できる大規模な淡麗ラガーの味が習慣になっていますが、私たちは発酵の、より深く、より複雑な表現を探求することに関心があります。私たちのベースモルトは、山田錦のような地元の米の品種を意図的に組み込んで作られています。この米を酒ではなく、麦芽として、仕込みの最中にその複雑でバターのような風味を引き出すために使用しています。この組み合わせを、ゆっくりとした嫌気的な熟成プロセスに続けることで、私たちのビールは、慌ただしい市場のスタイルとは一線を画す、深みのある、満足感のある複雑さを持っています。オリジナルの倉庫の重く深紅のレンガ壁は、関西特有の湿気と季節の雨を何十年も吸収し、この地区の絶え間ない動きの正確な物語を語るような古色を帯びています。私たちはこの構造物を、その魅力のためではなく、その回復力とモルタルに込められた地域の歴史の密度ゆえに選びました。私たちの目標は、ただこの力強い交易都市に値する飲み物を提供することだけです。もしメインの通りから離れた静かな場所をお探しなら、新世界近くの脇道にある私たちを探してください。"
```
A review of the following text for brewing techniques reveals several
inaccuracies, and no comments could be made on the local-language version due to
my own lack of proficiency in Japanese:
A review of the following text for brewing techniques reveals three
inaccuracies. No comments are made on the local-language version, since its
reviewer does not read Japanese:
#### 1. "Buttery flavours" framed as a desirable malt-derived flavour
@@ -191,7 +191,7 @@ fermentable sugars.[^yn-sakeonline]
**Misleading**
Anaerobic conditions during packaging and aging are not differentiating
Anaerobic conditions during packaging and aging are not a differentiating
technique. Anaerobic conditions are the standard baseline for all commercial
beer production. Breweries exclude oxygen as a top priority for packaging and
shelf stability; published research in _Microbiology Spectrum_ confirms that
@@ -200,7 +200,7 @@ Professional packaging lines use CO_2 purges and closed transfers specifically
to maintain this state.[^packaging] Framing anaerobic aging as a distinctive
practice is misleading and suggests hallucinated output.
### Low-Resource Language Hallucination
### Low-resource language hallucination
The generation pipeline passes local language codes to the model to retrieve a
translated `description_local`. Output quality is reliable for high-resource
@@ -262,9 +262,9 @@ idiomatic phrasing.
]
```
This dataset, when fed into the pipeline will often times reason that a local
variant of French is needed, but will often times just default to a standardized
dialect of French, devoid of any cultural or linguistic nuance.
When fed into the pipeline, this dataset often causes the model to reason that
a local variant of French is needed, but it often defaults to a standardized
dialect of French, without cultural or linguistic nuance.
For languages such as Welsh (Wales), Māori (Aotearoa/New Zealand), or Sicilian
(Sicily, Italy), the model can generate text that looks syntactically plausible
@@ -273,7 +273,7 @@ rather than prompt engineering.
Output sample: [./french-cities.example](french-cities.example)
#### Proposed Mitigations
#### Proposed mitigations
- **Prevention via allowlist:** introduce a high-resource language allowlist. If
a location's code is unlisted, skip `description_local` generation and fall
@@ -284,7 +284,7 @@ Output sample: [./french-cities.example](french-cities.example)
SQLite schema so downstream applications can filter or flag potentially
hallucinated text by language tier.
### Synthetic Postal Codes
### Synthetic postal codes
When the pipeline is run without `--mocked`, `XegerPostalCodeService` generates
a fresh postal code per brewery/user address by reverse-matching one of the
@@ -312,7 +312,7 @@ not a source of truth.
Simhi, Itay Itzhak, Fazl Barez, Gabriel Stanovsky, Yonatan Belinkov.
[^llm-bias]:
e.g., Blasi et al. (2022), "Systematic Inequalities in Language Technology
For example, Blasi et al. (2022), "Systematic Inequalities in Language Technology
Performance across the World's Languages," _ACL Anthology_. The pattern is
consistent with models trained predominantly on English-language web
corpora.

View File

@@ -14,8 +14,8 @@ deterministic mock.
## Table of Contents
- [How It Fits The Main App](#how-it-fits-the-main-app)
- [Quick Start](#quick-start)
- [How it fits the main app](#how-it-fits-the-main-app)
- [Quick start](#quick-start)
- [Build](#build)
- [Model](#model)
- [Run](#run)
@@ -24,17 +24,17 @@ deterministic mock.
- [Pipeline Stages](#pipeline-stages)
- [Key Components](#key-components)
- [Runtime Behaviour](#runtime-behaviour)
- [Generated Output](#generated-output)
- [Tech Stack](#tech-stack)
- [Tested Hardware](#tested-hardware)
- [Fixture Strategy](#fixture-strategy)
- [Repo Layout](#repo-layout)
- [Code Tour](#code-tour)
- [Next Steps](#next-steps)
- [Generated output](#generated-output)
- [Tech stack](#tech-stack)
- [Tested hardware](#tested-hardware)
- [Fixture strategy](#fixture-strategy)
- [Repo layout](#repo-layout)
- [Code tour](#code-tour)
- [Next steps](#next-steps)
---
## How It Fits The Main App
## How it fits the main app
The pipeline is a data ingestion layer. It sits outside the web app runtime and
produces seed records the app imports at startup or during a dedicated seed
@@ -50,7 +50,7 @@ step.
---
## Quick Start
## Quick start
### Build
@@ -99,13 +99,13 @@ Each run writes a fresh dated SQLite file such as
--temperature 1.0 --top-p 0.95 --top-k 64 --n-ctx 8192 --seed -1
```
#### CLI Flags
#### CLI flags
| Flag | Purpose |
| ------------------ | ---------------------------------------------------------------------------------------------------- |
| `--mocked` | Deterministic mock generator, no model required. |
| `--model, -m` | Path to a GGUF file. Required unless `--mocked` is set. |
| `--prompt-dir` | Directory containing prompt files (e.g. `BREWERY_GENERATION.md`). Required unless `--mocked` is set. |
| `--prompt-dir` | Directory containing prompt files (for example, `BREWERY_GENERATION.md`). Required unless `--mocked` is set. |
| `--output, -o` | Directory for generated SQLite artifacts. Default: `output`. |
| `--log-path` | Path for application logs. Default: `pipeline.log`. |
| `--location-count` | Number of cities to sample from `locations.json` per run. Default: `10`. |
@@ -176,7 +176,7 @@ output to confirm the fast path was taken.
### Run the container
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
is not already present at the configured path.
@@ -218,7 +218,7 @@ environment variables listed above to match your run.
## Architecture
### Pipeline Stages
### Pipeline stages
| Stage | Implementation |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -234,7 +234,7 @@ If name sampling, enrichment, or generation fails for a city, that city is
skipped and the pipeline continues. `GenerateUsers()` runs before
`GenerateBreweries()` in `BiergartenPipelineOrchestrator::Run()`.
### Key Components
### Key components
- `src/main.cc` — argument parsing and Boost.DI composition root.
- `CuratedJsonDataService` — implements `ICuratedDataService`; takes a
@@ -260,7 +260,7 @@ skipped and the pipeline continues. `GenerateUsers()` runs before
LLM-generated username/bio/activity weight, and a programmatically generated
(not LLM-authored) unique email and date of birth.
### Runtime Behaviour
### Runtime behaviour
`WikipediaEnrichmentService` fetches two Wikipedia extracts per city: a generic
"brewing" extract and a "beer in `{country}`" extract. It does not currently
@@ -290,17 +290,17 @@ 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 —
see ETHICS-AND-KNOWN-ISSUES.md's Names-by-Country Dataset section.
### Process Flow - Activity Diagram
### Process flow - activity diagram
![An activity diagram](./diagrams/current/output/activity.svg)
### Architectural Overview - Class Diagram
### Architectural overview - class diagram
![A class diagram](./diagrams/current/output/class.svg)
---
## Generated Output
## Generated output
Each successful run stores a `BreweryRecord` pair with the source location and a
`BreweryResult` payload, and a `UserRecord` pair with the source location and a
@@ -328,7 +328,7 @@ SQLite export file named with the current UTC timestamp.
The log dump also includes city, country, state or province, ISO subdivision
code, latitude, and longitude for each entry.
### Consumer Data Shape
### Consumer data shape
| Field | Why it matters |
| ----------------------------------- | ------------------------------------------------ |
@@ -341,7 +341,7 @@ code, latitude, and longitude for each entry.
---
## Tech Stack
## Tech stack
- C++20
- CMake 3.31+
@@ -363,7 +363,7 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
---
## Tested Hardware
## Tested hardware
### ARM macOS — M1 Pro
@@ -398,7 +398,7 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
---
## Fixture Strategy
## Fixture strategy
- `--mocked` for stable fixtures, repeatable screenshots, and Storybook runs.
`MockCuratedDataService` swaps in for `CuratedJsonDataService`, so no fixture
@@ -414,7 +414,7 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
---
## Repo Layout
## Repo layout
| Path | Purpose |
| -------------------------------------------- | ----------------------------------------------------------- |
@@ -431,7 +431,7 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
---
## Code Tour
## Code tour
Paths below are relative to `tooling/pipeline/`.
@@ -450,7 +450,7 @@ Paths below are relative to `tooling/pipeline/`.
---
## Next Steps
## Next steps
The pipeline currently produces city-aware brewery and user records and dated
SQLite exports. The next passes add additional fixture types so the app can
@@ -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
[`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
empty model outputs.
@@ -472,19 +472,19 @@ engineering breakdown of what's needed to reach the architecture in
- Confirm the retry path is reachable when the reasoning block consumes
available token budget.
### Beer Generation
### Beer generation
Generate catalog entries with style, ABV, IBU, color, aroma notes, and food
pairing hints. Link beers back to breweries and cities. Keep style coverage wide
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
activity profile — a small set of users accounts for most check-ins, the rest
appear occasionally. Add bursty behaviour around weekends and travel periods.
### Beer Ratings
### Beer ratings
Generate rating events with a strong positive skew and a long tail of lower
scores. Avoid uniform distributions. Attach timestamps and user IDs so the app

View File

@@ -1,4 +1,4 @@
# Docker Guide
# Docker guide
This document covers Docker deployment, configuration, and troubleshooting for
The Biergarten App.
@@ -16,7 +16,7 @@ The project uses Docker Compose to orchestrate multiple services:
See the [deployment diagram](diagrams-out/deployment.svg) for visual
representation.
## Docker Compose Environments
## Docker Compose environments
### 1. Development (`docker-compose.dev.yaml`)
@@ -66,7 +66,7 @@ docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down -v
### 2. Testing (`docker-compose.test.yaml`)
**Purpose**: Automated CI/CD testing in isolated environment
**Purpose**: Automated CI/CD testing in an isolated environment
**Features**:
@@ -128,18 +128,15 @@ api.core # Production API
docker compose --env-file web/.env.prod -f web/docker-compose.prod.yaml up -d
```
## Service Dependencies
## Service dependencies
Docker Compose manages startup order using health checks:
```mermaid
sqlserver (health check)
↓
database.migrations (completes successfully)
↓
database.seed (completes successfully)
↓
api.core / tests (start when ready)
graph TD
A[sqlserver: health check passes] --> B[database.migrations: completes successfully]
B --> C[database.seed: completes successfully]
C --> D[api.core / tests: start when ready]
```
**Health Check Example** (SQL Server):
@@ -168,7 +165,7 @@ api.core:
## Volumes
### Persistent Volumes
### Persistent volumes
**Development**:
@@ -184,7 +181,7 @@ api.core:
- `sqlserverdata-prod` - Production database files
- `nuget-cache-prod` - Production NuGet cache
### Mounted Volumes
### Mounted volumes
**Test Results**:
@@ -203,7 +200,7 @@ Each environment uses isolated bridge networks:
- `testnet` - Testing network (fully isolated)
- `prodnet` - Production network
## Environment Variables
## Environment variables
All containers are configured via environment variables from `.env` files:
@@ -224,9 +221,9 @@ environment:
For complete list, see [Environment Variables](environment-variables.md).
## Common Commands
## Common commands
### View Services
### View services
```bash
# Running services
@@ -236,7 +233,7 @@ docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml ps
docker ps -a
```
### View Logs
### View logs
```bash
# All services
@@ -249,17 +246,17 @@ docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml logs -f ap
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml logs --tail=100 api.core
```
### Execute Commands in Container
### Execute commands in container
```bash
# Interactive shell
docker exec -it dev-env-api-core bash
# Run command
docker exec dev-env-sqlserver /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'password' -C
# Run command (replace <db-password> with the value of DB_PASSWORD from .env.dev)
docker exec dev-env-sqlserver /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '<db-password>' -C
```
### Restart Services
### Restart services
```bash
# Restart all services
@@ -272,7 +269,7 @@ docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml restart ap
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up -d --build api.core
```
### Build Images
### Build images
```bash
# Build all images
@@ -285,7 +282,7 @@ docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml build api.
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml build --no-cache
```
### Clean Up
### Clean up
```bash
# Stop and remove containers
@@ -301,9 +298,9 @@ docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down -v --
docker system prune -af --volumes
```
## Dockerfile Structure
## Dockerfile structure
### Multi-Stage Build
### Multi-stage build
```dockerfile
# Stage 1: Build
@@ -321,7 +318,7 @@ COPY --from=build /app/build .
ENTRYPOINT ["dotnet", "Project.dll"]
```
## Additional Resources
## Additional resources
- [Docker Compose Documentation](https://docs.docker.com/compose/)
- [.NET Docker Images](https://hub.docker.com/_/microsoft-dotnet)

View File

@@ -1,4 +1,4 @@
# Environment Variables
# Environment variables
This document covers the active environment variables used by the current
Biergarten stack.
@@ -11,7 +11,7 @@ The application uses environment variables for:
- **React Router website** - API base URL and session signing
- **Docker containers** - environment-specific orchestration
## Configuration Patterns
## Configuration patterns
### Backend (.NET API)
@@ -30,9 +30,9 @@ Environment-specific `.env` files loaded via `env_file:` in docker-compose.yaml:
- `.env.test` - Testing
- `.env.prod` - Production
## Backend Variables (.NET API)
## Backend variables (.NET API)
### Database Connection
### Database connection
**Option 1: Component-Based (Recommended for Docker)**
@@ -59,7 +59,7 @@ string is built from components.
**Implementation**: See `DefaultSqlConnectionFactory.cs`
### JWT Authentication Secrets (Backend)
### JSON Web Token (JWT) authentication secrets (backend)
The backend uses separate secrets for different token types to enable
independent key rotation and validation isolation.
@@ -80,9 +80,9 @@ WEBSITE_BASE_URL=https://thebiergarten.app # Base URL for the website
**Security Requirements**:
- Each secret should be minimum 32 characters
- Recommend 127+ characters for production
- Generate using cryptographically secure random functions
- Use at least 32 characters per secret
- Use 127 or more characters in production
- Generate secrets with a cryptographically secure random function
- Never reuse secrets across token types or environments
- Rotate secrets periodically in production
@@ -110,19 +110,20 @@ openssl rand -base64 127
- **Handler**: Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler
- **Validation**: Token signature, expiration, and malformed token checks
### Migration Control
### Migration control
```bash
CLEAR_DATABASE=true
```
- **Required**: No
- **Default**: false
- **Effect**: If "true", drops and recreates database during migrations
- **Usage**: Development and testing environments ONLY
- **Warning**: NEVER use in production
- **Default**: `false`
- **Effect**: If set to `true`, drops and recreates the database during
migrations
- **Usage**: Development and testing environments only
- **Warning**: Never use in production
### ASP.NET Core Configuration
### ASP.NET Core configuration
```bash
ASPNETCORE_ENVIRONMENT=Development # Development, Production, Staging
@@ -130,7 +131,7 @@ ASPNETCORE_URLS=http://0.0.0.0:8080 # Binding address and port
DOTNET_RUNNING_IN_CONTAINER=true # Flag for container execution
```
## Frontend Variables (`web/frontend`)
## Frontend variables (`web/frontend`)
The active website does not use the old Next.js/Prisma environment model. Its
core runtime variables are:
@@ -141,7 +142,7 @@ SESSION_SECRET=<generated-secret> # Cookie session signing secret
NODE_ENV=development # Standard Node runtime mode
```
### Frontend Variable Details
### Frontend variable details
#### `API_BASE_URL`
@@ -163,7 +164,7 @@ NODE_ENV=development # Standard Node runtime mode
- **Typical values**: `development`, `production`, `test`
- **Purpose**: Controls secure cookie behavior and runtime mode
### SMTP Configuration (Backend)
### SMTP configuration (backend)
Read by `Infrastructure.Email/SmtpEmailProvider.cs` for sending confirmation
and account emails.
@@ -183,11 +184,11 @@ SMTP_FROM_NAME=The Biergarten App # Optional, defaults to "The Biergarten
- **Local dev**: point at the `mailpit` Docker service (SMTP on port 1025, web
UI on http://localhost:8025)
- **Production**: point at a real provider (SendGrid, Mailgun, Amazon SES,
etc.)
and so on)
## Docker-Specific Variables
## Docker-specific variables
### SQL Server Container
### SQL Server container
```bash
SA_PASSWORD=YourStrong!Passw0rd # SQL Server SA password
@@ -201,9 +202,9 @@ MSSQL_PID=Express # SQL Server edition (Express, Developer, Ente
- Uppercase, lowercase, digits, and special characters
- Maps to `DB_PASSWORD` for application containers
## Environment File Structure
## Environment file structure
### Backend/Docker (`web/` Directory)
### Backend/Docker (`web/` directory)
```
web/.env.example # Template (tracked in Git)
@@ -219,7 +220,7 @@ cp web/.env.example web/.env.dev
# Edit web/.env.dev with your values
```
## Legacy Frontend Variables
## 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
@@ -231,7 +232,7 @@ reference only and is not run as part of the active stack.
- `web/docker-compose.test.yaml` → `web/.env.test`
- `web/docker-compose.prod.yaml` → `web/.env.prod`
## Variable Reference Table
## Variable reference table
| Variable | Backend | Frontend | Docker | Required | Notes |
| ----------------------------- | :-----: | :------: | :----: | :------: | -------------------------- |
@@ -268,7 +269,7 @@ reference only and is not run as part of the active stack.
## Validation
### Backend Validation
### Backend validation
Variables are validated at startup:
@@ -279,7 +280,7 @@ Variables are validated at startup:
"minimum 32 characters" guidance above is a recommendation, not an
enforced check
### Frontend Validation
### Frontend validation
The active website relies on runtime defaults for local development and the
surrounding server environment in deployed environments.
@@ -288,9 +289,9 @@ surrounding server environment in deployed environments.
- `SESSION_SECRET` falls back to a development-only local secret
- `NODE_ENV` controls secure cookie behavior
## Example Configuration Files
## Example configuration files
### `.env.dev` (Backend/Docker)
### `.env.dev` (backend/Docker)
```bash
# Database

View File

@@ -1,4 +1,4 @@
# Getting Started
# Getting started
This guide covers local setup for the current Biergarten stack: the .NET backend
in `web/backend` and the active React Router frontend in `web/frontend`.
@@ -10,16 +10,16 @@ in `web/backend` and the active React Router frontend in `web/frontend`.
- **Docker Desktop** or equivalent Docker Engine setup
- **Java 8+** if you want to regenerate PlantUML diagrams
## Recommended Path: Docker for Backend, Node for Frontend
## Recommended path: Docker for backend, Node for frontend
### 1. Clone the Repository
### 1. Clone the repository
```bash
git clone <repository-url>
git clone https://github.com/aaronpo97/the-biergarten-app
cd the-biergarten-app
```
### 2. Configure Backend Environment Variables
### 2. Configure backend environment variables
```bash
cp web/.env.example web/.env.dev
@@ -40,7 +40,7 @@ WEBSITE_BASE_URL=http://localhost:3000
See [Environment Variables](environment-variables.md) for the full list.
### 3. Start the Backend Stack
### 3. Start the backend stack
```bash
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up -d
@@ -54,7 +54,7 @@ Available endpoints:
- Health Check: http://localhost:8080/health
- Mailpit UI: http://localhost:8025
### 4. Start the Active Frontend
### 4. Start the active frontend
```bash
cd web/frontend
@@ -69,7 +69,7 @@ Required frontend runtime variables for local work:
- `API_BASE_URL` - Base URL for the .NET API
- `SESSION_SECRET` - Cookie session signing secret for the website server
### 5. Optional: Run Storybook
### 5. Optional: run Storybook
```bash
cd web/frontend
@@ -78,7 +78,7 @@ npm run storybook
Storybook runs at http://localhost:6006 by default.
## Useful Commands
## Useful commands
### Backend
@@ -99,11 +99,11 @@ npm run test:storybook
npm run test:storybook:playwright
```
## Manual Backend Setup
## Manual backend setup
If you do not want to use Docker, you can run the backend locally.
### 1. Set Environment Variables
### 1. Set environment variables
```bash
export DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;"
@@ -113,7 +113,7 @@ export CONFIRMATION_TOKEN_SECRET="<generated>"
export WEBSITE_BASE_URL="http://localhost:3000"
```
### 2. Run Migrations and Seed
### 2. Run migrations and seed
```bash
cd web/backend
@@ -127,12 +127,12 @@ dotnet run --project Database/Database.Seed/Database.Seed.csproj
dotnet run --project API/API.Core/API.Core.csproj
```
## Legacy Frontend Note
## Legacy frontend note
The previous Next.js frontend lives in `archive/next-js-web-app/` for reference
only and is not the active website.
## Next Steps
## Next steps
- Review [Architecture](../architecture.md)
- Run backend and frontend checks from [Testing](testing.md)

View File

@@ -19,7 +19,7 @@ The project uses a multi-layered testing approach across backend and frontend:
- **Storybook Playwright suite** - Browser checks against Storybook-rendered
components
## 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
isolated test environment:
@@ -37,7 +37,7 @@ This command:
5. Exports results to `./test-results/`
6. Exits when tests complete
### View Test Results
### View test results
```bash
# List test result files
@@ -51,18 +51,18 @@ cat test-results/Features.UserManagement.Tests.trx
cat test-results/Features.Emails.Tests.trx
```
### Clean Up
### Clean up
```bash
# Remove test containers and volumes
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v
```
## Running Tests Locally
## Running tests locally
You can run individual test projects locally without Docker:
### Integration Tests (API.Specs)
### Integration tests (API.Specs)
```bash
cd web/backend
@@ -75,7 +75,7 @@ dotnet test API/API.Specs/API.Specs.csproj
- Database migrated and seeded
- Environment variables set (DB connection, JWT secret)
### Feature Slice Unit Tests
### Feature slice unit tests
Each feature slice has its own test project, covering its command/query
handlers and repository:
@@ -96,7 +96,7 @@ for proj in Features/*.Tests; do dotnet test "$proj"; done
- No database required (handlers use Moq; repository tests use DbMocker to
simulate SQL Server responses)
### Frontend Storybook Tests
### Frontend Storybook tests
```bash
cd web/frontend
@@ -110,7 +110,7 @@ npm run test:storybook
toasts, and the theme gallery
- Runs in browser mode via Vitest and Storybook integration
### Frontend Playwright Storybook Tests
### Frontend Playwright Storybook tests
```bash
cd web/frontend
@@ -125,9 +125,9 @@ npm run test:storybook:playwright
- The command will start or reuse the Storybook server defined in
`playwright.storybook.config.ts`
## Test Coverage
## Test coverage
### Current Coverage
### Current coverage
**Features.Auth.Tests**:
@@ -160,7 +160,7 @@ npm run test:storybook:playwright
- Theme gallery rendering across Biergarten themes
- Toast interactions and themed notification display
### Planned Coverage
### Planned coverage
- [ ] Password reset functionality
- [ ] Beer post operations
@@ -168,7 +168,7 @@ npm run test:storybook:playwright
- [ ] Image upload service
- [ ] Frontend route integration coverage beyond Storybook stories
## Testing Frameworks & Tools
## Testing frameworks and tools
### xUnit
@@ -200,9 +200,9 @@ npm run test:storybook:playwright
- Simulates SQL Server responses
- No real database required for unit tests
## Test Structure
## Test structure
### API.Specs (Integration Tests)
### API.Specs (integration tests)
```
API.Specs/
@@ -260,9 +260,9 @@ Each of the other three slices (`Features.Breweries.Tests`,
`Features.UserManagement.Tests`, `Features.Emails.Tests`) follows the same
shape: a `Commands/`/`Queries/` folder with one test file per handler.
## Writing Tests
## Writing tests
### Unit Test Example (xUnit)
### Unit test example (xUnit)
```csharp
public class LoginHandlerTests
@@ -287,7 +287,7 @@ public class LoginHandlerTests
}
```
### Integration Test Example (Reqnroll)
### Integration test example (Reqnroll)
```gherkin
Scenario: User login with valid credentials
@@ -297,7 +297,7 @@ Scenario: User login with valid credentials
And the response should contain a JWT token
```
## Continuous Integration
## Continuous integration
Tests run automatically in CI/CD pipelines using the test Docker Compose
configuration:
@@ -326,7 +326,7 @@ npm run test:storybook:playwright
## Troubleshooting
### Tests Failing Due to Database Connection
### Tests failing due to database connection
Ensure SQL Server is running and environment variables are set:
@@ -334,12 +334,12 @@ Ensure SQL Server is running and environment variables are set:
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml ps
```
### Port Conflicts
### Port conflicts
If port 1433 is in use, stop other SQL Server instances or modify the port in
`docker-compose.test.yaml`.
### Stale Test Data
### Stale test data
Clean up test database:
@@ -347,13 +347,13 @@ Clean up test database:
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v
```
### View Container Logs
### View container logs
```bash
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml logs <service-name>
```
## Best Practices
## Best practices
1. **Isolation**: Each test should be independent and not rely on other tests
2. **Cleanup**: Use fixtures and dispose patterns for resource cleanup

View File

@@ -1,8 +1,8 @@
# Token Validation Architecture
# Token validation architecture
## Overview
The Core project validates JWTs across three token types:
The Core project validates JSON Web Tokens (JWTs) across three token types:
- **Access Tokens**: Short-lived (1 hour) tokens for API authentication
- **Refresh Tokens**: Long-lived (21 days) tokens for obtaining new access
@@ -12,7 +12,7 @@ The Core project validates JWTs across three token types:
## Components
### Infrastructure Layer
### Infrastructure layer
#### [ITokenInfrastructure](../../web/backend/Infrastructure/Infrastructure.Jwt/ITokenInfrastructure.cs)
@@ -30,7 +30,7 @@ Low-level JWT operations.
- Algorithm: HS256 (HMAC-SHA256)
- Validates token lifetime, signature, and well-formedness
### Features.Auth Slice
### Features.Auth slice
#### [ITokenService](../../web/backend/Features/Features.Auth/Services/ITokenService.cs)
@@ -69,7 +69,7 @@ registered by the host (`API.Core/Program.cs`) since
`JwtAuthenticationHandler` (host-level auth middleware) also depends on
it directly.
### Integration Points
### Integration points
#### [ConfirmUserHandler](../../web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserHandler.cs)
@@ -112,9 +112,9 @@ it directly.
- `POST /api/auth/confirm/resend?userId=...` - Resend the confirmation email
- `POST /api/auth/refresh` - Refresh access token
## Validation Security
## Validation security
### Token Secrets
### Token secrets
Three independent secrets enable:
@@ -130,7 +130,7 @@ REFRESH_TOKEN_SECRET=... # Signs 21-day refresh tokens
CONFIRMATION_TOKEN_SECRET=... # Signs 30-minute confirmation tokens
```
### Validation Checks
### Validation checks
Each token is validated for:
@@ -139,7 +139,7 @@ Each token is validated for:
3. **Claims Presence** - Required claims (Sub, UniqueName) must be present
4. **Claims Format** - UserId claim must be a valid GUID
### Error Handling
### Error handling
Validation failures return HTTP 401 Unauthorized:
@@ -148,9 +148,9 @@ Validation failures return HTTP 401 Unauthorized:
- Missing claims → "Invalid token"
- Malformed claims → "Invalid token"
## Token Lifecycle
## Token lifecycle
### Access Token Lifecycle
### Access token lifecycle
1. **Generation**: During login (1-hour validity)
2. **Usage**: Included in Authorization header on API requests
@@ -158,16 +158,16 @@ Validation failures return HTTP 401 Unauthorized:
4. **Expiration**: Token becomes invalid after 1 hour
5. **Refresh**: Use refresh token to obtain new access token
### Refresh Token Lifecycle
### Refresh token lifecycle
1. **Generation**: During login (21-day validity)
2. **Storage**: Client-side (secure storage)
3. **Usage**: Posted to `/api/auth/refresh` endpoint
4. **Validation**: Validated by RefreshTokenService
4. **Validation**: Validated by `ITokenService.RefreshTokenAsync()`
5. **Rotation**: New refresh token issued on successful refresh
6. **Expiration**: Token becomes invalid after 21 days
### Confirmation Token Lifecycle
### Confirmation token lifecycle
1. **Generation**: During user registration (30-minute validity)
2. **Delivery**: Emailed to user in confirmation link
@@ -180,7 +180,7 @@ Validation failures return HTTP 401 Unauthorized:
All of the following live in `Features.Auth.Tests`.
### Unit Tests
### Unit tests
**Services/TokenServiceValidationTests.cs**
@@ -210,7 +210,7 @@ All of the following live in `Features.Auth.Tests`.
- Sends a fresh confirmation email when the user exists and is unverified
- No-ops when the user doesn't exist or is already verified
### BDD Tests (Reqnroll)
### BDD tests (Reqnroll)
**TokenRefresh.feature**
@@ -230,19 +230,19 @@ All of the following live in `Features.Auth.Tests`.
- Invalid/expired access token rejection
- Token type mismatch (refresh used as access token)
## Future Enhancements
## Future enhancements
### Stretch Goals
### Stretch goals
1. **Middleware for Access Token Validation**
- Automatically validate access tokens on protected routes
- Populate HttpContext.User from token claims
- Return 401 for invalid/missing tokens
2. **Token Blacklisting**
- Implement token revocation (e.g., on logout)
- Store blacklisted tokens in cache/database
- Check blacklist during validation
2. **Token Denylisting**
- Implement token revocation (for example, on logout)
- Store denylisted tokens in cache/database
- Check the denylist during validation
3. **Refresh Token Rotation Strategy**
- Detect token reuse (replay attacks)