diff --git a/README.md b/README.md
index ce83a51a..9cb41d9a 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ Data generation pipeline (C++):
Active areas in the repository:
- .NET 10 backend (vertical-slice architecture with MediatR) + SQL Server
-- React 19 website (React Router 7 + Vite)
+- React 19 website (React Router 8 + Vite)
- Shared Biergarten theme system + Storybook coverage
- Auth flows and account/email integration (local Mailpit in dev compose)
- Photo upload to S3-compatible object storage (local SeaweedFS in dev compose)
@@ -54,7 +54,7 @@ Archived/reference areas:
## Tech stack
- **Backend**: .NET 10, ASP.NET Core, SQL Server 2022, DbUp
-- **Frontend**: React 19, React Router 7, Vite 7, Tailwind CSS 4, DaisyUI 5,
+- **Frontend**: React 19, React Router 8, Vite 7, Tailwind CSS 4, DaisyUI 5,
Leaflet/React Leaflet (brewery maps)
- **UI Documentation**: Storybook 10, Vitest browser mode, Playwright
- **Testing**: xUnit, Reqnroll (BDD), FluentAssertions, Moq
@@ -66,7 +66,7 @@ Archived/reference areas:
## Requirements
- .NET SDK 10 or later
-- Node.js 18 or later
+- Node.js 22 or later
- Docker Desktop, or an equivalent Docker Engine setup
See [Getting Started](docs/web/getting-started.md) for the full
@@ -83,10 +83,15 @@ This section is the shortest path to a working dev environment.
git clone https://github.com/aaronpo97/the-biergarten-app
cd the-biergarten-app
-cp web/.env.example web/.env.dev
+cd web && ./generate-env.sh .env.dev && cd ..
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up --build -d
```
+`generate-env.sh` copies `.env.example` and replaces the secret-bearing values
+with freshly randomized ones; a plain `cp` leaves placeholder secrets in
+place. See [Environment Variables](docs/web/environment-variables.md#generating-an-env-file)
+for details.
+
Access:
- Website: http://localhost:3000
@@ -137,7 +142,7 @@ Create a test environment file, then run the full test stack (backend and
frontend) with Docker:
```bash
-cp web/.env.example web/.env.test
+cd web && ./generate-env.sh .env.test && cd ..
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up --abort-on-container-exit
```
diff --git a/docs/web/architecture.md b/docs/web/architecture.md
index c43dcab6..ad6faabf 100644
--- a/docs/web/architecture.md
+++ b/docs/web/architecture.md
@@ -8,7 +8,7 @@ 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
-- **Frontend**: React 19 + React Router 7 website in `web/frontend`
+- **Frontend**: React 19 + React Router 8 website in `web/frontend`
- **Architecture Style**: Vertical-slice backend plus server-rendered React
frontend
@@ -96,6 +96,7 @@ flowchart TB
HOST -- "discovers controllers via
AddApplicationPart" --> SLICES
S1 -- "SendRegistrationEmailCommand
(via Shared.Application contract)" --> S3
+ S1 -- "ProjectReference
(avatar upload)" --> S5
SLICES --> SHARED
SLICES --> DOMAIN
@@ -108,11 +109,13 @@ flowchart TB
style S5 stroke-dasharray: 4 3
```
-Slices never reference each other's project. The one cross-slice interaction
-(`Features.Users` triggering a confirmation email handled by `Features.Emails`)
-goes through a MediatR command (`SendRegistrationEmailCommand`) whose contract
-lives in `Shared.Application`, so neither slice takes a project reference on the
-other.
+Slices generally don't reference each other's project. Most cross-slice
+interactions go through a MediatR command instead — for example,
+`Features.Users` triggering a confirmation email handled by `Features.Emails`
+via a `SendRegistrationEmailCommand` whose contract lives in
+`Shared.Application`, so neither slice takes a project reference on the other.
+The one exception is `Features.Users`, which takes a direct `ProjectReference`
+on `Features.PhotoUpload` to send `UploadPhotoCommand` for avatar uploads.
### Layer responsibilities
@@ -173,13 +176,14 @@ brewery photo upload command) rather than bound to an HTTP route directly.
**Dependencies**:
- `Domain.Entities`, `Domain.Exceptions`
-- `Infrastructure.Sql` (generic ADO.NET plumbing) plus whichever infrastructure
- project the slice needs (`Infrastructure.Jwt`/
+- `Database.Connection` (generic ADO.NET connection/command plumbing) plus
+ whichever infrastructure project the slice needs (`Infrastructure.Jwt`/
`Infrastructure.PasswordHashing` for Users, `Infrastructure.Email`/
`Infrastructure.Email.Templates` for Emails, `Infrastructure.FileUpload` for
PhotoUpload)
- `Shared.Contracts`, `Shared.Application`
-- **Never** another `Features.*` project
+- No other `Features.*` project, with one exception: `Features.Users`
+ references `Features.PhotoUpload` directly for avatar uploads
**Rules**:
@@ -375,7 +379,7 @@ error-handling approach in more detail.
### Website (`web/frontend`)
-The website is a React Router 7 application with server-side rendering enabled.
+The website is a React Router 8 application with server-side rendering enabled.
```text
web/frontend/
@@ -395,7 +399,7 @@ web/frontend/
│ │ - home landing page
│ │ - theme theme switcher/guide
│ ├── hooks/ Shared hooks not tied to one feature, for example useMediaQuery
-│ ├── routes.ts Route table (React Router 7 config-based routing)
+│ ├── routes.ts Route table (React Router 8 config-based routing)
│ ├── root.tsx App shell and global providers
│ └── app.css Theme tokens and global styling
├── .storybook/ Storybook config and preview setup
@@ -471,7 +475,7 @@ reference. Product and engineering documentation points to `web/frontend`.
- Memory: 64MB
- Iterations: 4
-- Parallelism: CPU core count
+- Parallelism: 2 (hardcoded, to avoid thread-pool exhaustion)
- Salt: 128-bit (16 bytes)
- Hash: 256-bit (32 bytes)
@@ -499,6 +503,12 @@ reference. Product and engineering documentation points to `web/frontend`.
}
```
+This block exists in `appsettings.json`, but nothing in the code currently
+reads it: actual token lifetimes come from the hardcoded
+`TokenServiceExpirationHours` constants, and `JwtInfrastructure` hardcodes
+`ValidateIssuer`/`ValidateAudience` to `false`, so `Issuer`/`Audience` are not
+enforced.
+
## Database architecture
For the table-by-table schema and the custom SQL error code scheme, see
diff --git a/docs/web/environment-variables.md b/docs/web/environment-variables.md
index 09dea3ac..232f0fde 100644
--- a/docs/web/environment-variables.md
+++ b/docs/web/environment-variables.md
@@ -302,10 +302,16 @@ web/.env.prod # Production config (gitignored)
**Setup**:
```bash
-cp web/.env.example web/.env.dev
-# Edit web/.env.dev with your values
+cd web
+./generate-env.sh .env.dev
```
+See [Generating an env file](#generating-an-env-file) above — `generate-env.sh`
+randomizes the secret-bearing values, so it's the preferred way to create any
+`.env.dev`/`.env.test`/`.env.prod` file. A plain `cp web/.env.example
+web/.env.dev` works too, but leaves the placeholder secrets from the template
+in place; edit them manually if you go that route.
+
**Docker Compose Mapping**:
- `web/docker-compose.dev.yaml` → `web/.env.dev`
diff --git a/docs/web/getting-started.md b/docs/web/getting-started.md
index ad5027f3..26aee44c 100644
--- a/docs/web/getting-started.md
+++ b/docs/web/getting-started.md
@@ -13,7 +13,7 @@ This guide covers local setup for the Biergarten stack: the .NET backend in
## Prerequisites
- **.NET SDK 10+**
-- **Node.js 18+**
+- **Node.js 22+**
- **Docker Desktop** or equivalent Docker Engine setup
- **Java 8+** if you want to regenerate PlantUML diagrams