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
This document describes the active architecture of The Biergarten App.
This document describes the architecture of The Biergarten App.
## High-level overview
The Biergarten App is a monorepo with a clear split between the backend and the
active website:
The Biergarten App is a monorepo split between the backend and the website:
- **Backend**: .NET 10 Web API with SQL Server, organized as vertical feature
slices with MediatR
@@ -13,8 +12,8 @@ active website:
- **Architecture Style**: Vertical-slice backend plus server-rendered React
frontend
The legacy Next.js frontend has been retained in `archive/next-js-web-app/` for
reference only.
`archive/next-js-web-app/` contains an archived Next.js frontend, kept for
reference.
## Diagrams
@@ -24,11 +23,25 @@ For visual representations, see:
architecture diagram
- [deployment.svg](website/diagrams-out/deployment.svg) - Docker deployment
diagram
- [authentication-flow.svg](website/diagrams-out/authentication-flow.svg) -
Authentication workflow
- [database-schema.svg](website/diagrams-out/database-schema.svg) - Database
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
### 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
project that owns its own MediatR commands/queries/handlers, validators, 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:
```
@@ -128,12 +141,12 @@ own
`Features.Emails` has no `Controllers/` folder. It's invoked only via MediatR
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
`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
`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
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
@@ -320,12 +333,12 @@ 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, without an ORM's change-tracking or mapping magic
it
**Strategy**:
- No ORM (Entity Framework not used); each repository issues inline SQL
through Dapper, with no manual `IDbCommand` parameter binding. The one
- 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.
@@ -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
index backs the query yet; `schema.sql` sketches one, commented out, for
when the table grows large enough to need it
- Referential checks that a stored procedure used to perform (e.g. "does this
`CityId` exist?") are now explicit `SELECT 1 ...` existence checks in the
repository method, run inside the same transaction as the write
- 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
- Optimistic concurrency uses each table's `RowVersion` (`ROWVERSION`) column,
checked in the `UPDATE ... WHERE ... AND RowVersion = @RowVersion` clause
- Repositories throw `Domain.Exceptions` types (`NotFoundException`,
`ConflictException`, ...) directly when a check fails, rather than relying on
a database-side `THROW`; `API.Core`'s `GlobalExceptionFilter` maps those
exception types to HTTP status codes
`ConflictException`, ...) directly when a check fails; `API.Core`'s
`GlobalExceptionFilter` maps those exception types to HTTP status codes
- Application focuses on orchestration; the database enforces integrity via
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.
## 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.
```text
@@ -397,7 +409,7 @@ web/frontend/
### 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:
- Biergarten Lager
@@ -408,11 +420,10 @@ themes:
All component styling should prefer semantic tokens such as `primary`,
`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/`
for reference only. Active product and engineering documentation should point to
`web/frontend`.
`archive/next-js-web-app/` contains an archived Next.js frontend, kept for
reference. Product and engineering documentation points to `web/frontend`.
## Security architecture
@@ -431,8 +442,8 @@ for reference only. Active product and engineering documentation should point to
3. **Login**:
- User submits credentials
- Password verified against hash
- Access and refresh JWTs are issued; the refresh token is stored server-side
against the user's credential row
- Access and refresh JWTs are issued and returned to the client; neither is
stored server-side
4. **Token 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
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.**
---
@@ -102,7 +102,7 @@ vendored verbatim, unmodified, from
(the `common-forenames-by-country.json` / `common-surnames-by-country.json`
release assets), released under **CC0** (public domain). That dataset's own
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
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
@@ -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
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`,
`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.
---
@@ -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
`latitude`/`longitude` from `locations.json`.
The resulting point is **plausible by construction** — it always falls within
the city's vicinity — but it is not looked up from any real address registry.
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.

View File

@@ -7,12 +7,12 @@ deterministic mock.
> **This pipeline produces AI-generated data.** It is not a source of truth for
> 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.
---
## Table of Contents
## Table of contents
- [How it fits the main app](#how-it-fits-the-main-app)
- [Quick start](#quick-start)
@@ -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.
@@ -191,7 +191,7 @@ docker run --rm \
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
that path first — see [Model](#model) above.
that path first; see [Model](#model).
#### Environment variables
@@ -211,8 +211,9 @@ configurable via environment variable.
Use a GPU pod template. Mount persistent storage for `/workspace/models`,
`/workspace/output`, and `/workspace/logs`. See
`tooling/pipeline/runpod/pod-template.yaml` for a starter template — set the
environment variables listed above to match your run.
`tooling/pipeline/runpod/pod-template.yaml` for a starter template; set the
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
- `src/main.cc` — argument parsing and Boost.DI composition root.
- `CuratedJsonDataService` — implements `ICuratedDataService`; takes a
- `src/main.cc`: argument parsing and Boost.DI composition root.
- `CuratedJsonDataService`: implements `ICuratedDataService`; takes a
`CuratedDataFilePaths` DTO (locations/personas/forenames/surnames paths) in
its constructor, then parses and validates curated location, persona, and
forename/surname JSON, memoizing each result after its first load on a given
instance. `MockCuratedDataService` is the in-memory substitute (4 fixed
locations, 3 personas, and name data for `US`/`DE`/`FR`/`BE`) used in
`--mocked` runs.
- `WikipediaEnrichmentService` — queries Wikipedia extracts, caches results,
- `WikipediaEnrichmentService`: queries Wikipedia extracts, caches results,
returns empty context on failure. `MockEnrichmentService` is the no-op
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
times with corrective feedback in the retry prompt. The token budget is fixed
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.
- `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.
- Brewery payloads include English and local-language name and description
fields. User payloads carry a sampled first/last name and gender, an
@@ -281,13 +282,13 @@ runs.
`CuratedJsonDataService` memoizes each of `LoadLocations()`, `LoadPersonas()`,
`LoadForenamesByCountry()`, and `LoadSurnamesByCountry()` independently the
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.
`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
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.
### 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
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
> features modern C++20 throughout. Formatting follows the Google C++ Style
> Guide, with the exception of indentation width (3 spaces vs. Google's 2).
> **Code style:** The code follows the Google C++ Style Guide, except that
> indentation is 3 spaces instead of Google's 2, and uses C++20 throughout.
---
## 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 |
| 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 |
| 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
filtering.
- `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.
- Treat SQLite output as seed material for the app's brewery and user domains,
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/`.
- `src/main.cc` — argument parsing and DI composition root.
- `src/biergarten_pipeline_orchestrator/` — orchestration, sampling, logging,
- `src/main.cc`: argument parsing and DI composition root.
- `src/biergarten_pipeline_orchestrator/`: orchestration, sampling, logging,
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` used in `--mocked` runs.
- `src/services/enrichment/wikipedia/` — enrichment service and cache.
- `src/services/sqlite/` — SQLite export implementation.
- `src/data_generation/llama/` — local inference, prompt loading, output
- `src/services/enrichment/wikipedia/`: enrichment service and cache.
- `src/services/sqlite/`: SQLite export implementation.
- `src/data_generation/llama/`: local inference, prompt loading, output
validation.
- `src/data_generation/mock/` — deterministic fallback.
- `runpod/` — container build and runtime launcher.
- `src/data_generation/mock/`: deterministic fallback.
- `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
[`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.
@@ -481,7 +481,7 @@ enough to exercise search, sort, and category filters.
### 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
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

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
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 editing
an already-applied script means it won't re-run against existing databases.
editing an existing one in place: DbUp's tracking is per-script, so an edited
already-applied script never re-runs against existing databases.
## 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,
state/provinces, and cities (created on demand while resolving each brewery's
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.
## Database error handling
@@ -78,10 +78,10 @@ repository (`Features.PhotoUpload`) but no seed data.
- On failure, the repository throws a `Domain.Exceptions` type directly
(`NotFoundException`, `ConflictException`, `ArgumentException`, ...).
`API.Core`'s `GlobalExceptionFilter` maps these to HTTP status codes (404,
409, 400, ...); an unmapped `SqlException` (e.g. a genuine connectivity or
constraint-violation failure) maps to 503.
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`.
concurrently; see `DapperUserStore.IsDuplicateKeyViolation`.

View File

@@ -100,7 +100,8 @@ frontend.tests # Storybook Vitest + Playwright suites (own Playwright image)
```bash
# 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
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
```
`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`)
**Purpose**: Production-ready deployment

View File

@@ -1,7 +1,6 @@
# Environment variables
This document covers the active environment variables used by the current
Biergarten stack.
This document covers the environment variables used by the Biergarten stack.
## Overview
@@ -19,10 +18,10 @@ Direct environment variable access via `Environment.GetEnvironmentVariable()`.
### Frontend (`web/frontend`)
The active website reads runtime values from the server environment for its auth
and API integration. In Docker, the `frontend` service is wired into the same
The website reads runtime values from the server environment for its auth and
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
locally with `npm run dev` still reads from the shell environment.
locally with `npm run dev` reads from the shell environment.
### Docker
@@ -104,7 +103,8 @@ WEBSITE_BASE_URL=https://thebiergarten.app # Base URL for the website
**Security Requirements**:
- 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
- Never reuse secrets across token types or environments
- 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
```
`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`)
The active website does not use the old Next.js/Prisma environment model. Its
core runtime variables are:
The website's core runtime variables are:
```bash
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
```
## 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**:
- `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 |
| `CLEAR_DATABASE` | ✓ | | ✓ | No | Dev/test reset flag |
| `ASPNETCORE_ENVIRONMENT` | ✓ | | ✓ | Yes | ASP.NET environment |
| `ASPNETCORE_URLS` | ✓ | | ✓ | Yes | API binding address |
| `ASPNETCORE_URLS` | ✓ | | ✓ | Yes\*\* | API binding address |
| `SA_PASSWORD` | | | ✓ | Yes | SQL Server container |
| `ACCEPT_EULA` | | | ✓ | Yes | SQL Server EULA |
| `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`,
`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
### Backend validation
@@ -359,8 +364,8 @@ Variables are validated at startup:
### Frontend validation
The active website relies on runtime defaults for local development and the
surrounding server environment in deployed environments.
The website relies on runtime defaults for local development and on the server
environment when deployed.
- `API_BASE_URL` defaults to `http://localhost:8080`
- `SESSION_SECRET` falls back to a development-only local secret

View File

@@ -1,7 +1,7 @@
# 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`.
This guide covers local setup for the Biergarten stack: the .NET backend in
`web/backend` and the React Router frontend in `web/frontend`.
## Prerequisites
@@ -69,7 +69,7 @@ Available endpoints:
- Mailpit UI: http://localhost:8025
- SeaweedFS S3 API: http://localhost:8333
### 4. Start the active frontend
### 4. Start the frontend
```bash
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
```
## Legacy frontend note
## Archived frontend
The previous Next.js frontend lives in `archive/next-js-web-app/` for reference
only and is not the active website.
`archive/next-js-web-app/` contains an archived Next.js frontend, kept for
reference; it is not part of the running stack.
## 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)
The easiest way to run all tests is using Docker Compose, which sets up an
isolated test environment:
To run all tests, use Docker Compose, which sets up an isolated test
environment:
```bash
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
(`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.
torn down separately (see below); `wait` doesn't touch them.
### 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.
`./test-results/` is uploaded as a build artifact regardless of outcome.
Earlier versions of this workflow used
`up --abort-on-container-exit`, which stops the entire stack the instant
*any* watched container exits. That's wrong here: the one-shot
`database.migrations`/`database.seed` jobs are expected to exit 0 partway
through, and even scoping the flag to just the three test services doesn't
help, since `frontend.tests` (no database dependency) reliably finishes
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.
`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:
@@ -414,13 +411,13 @@ act push \
`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
- 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
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.
@@ -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
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
`.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:

View File

@@ -34,8 +34,7 @@ Low-level JWT operations.
#### [ITokenService](../../web/backend/Features/Features.Users/Services/ITokenService.cs)
Both token generation and validation live on the same slice-internal service
(there is no separate validation service/class).
Token generation and validation live on the same slice-internal service.
**Generation methods:**
@@ -79,9 +78,17 @@ 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<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
`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)
**Flow:**