4.9 KiB
Architecture decisions
Why the site is built the way it is. Read this before questioning one of these
decisions — each records the reasoning, not just the choice. Operational how-to
lives in docs/deployment.md; open work lives in the Gitea issues (milestone
„Elternbeirat-Website").
Note on scope. The site started as a static information site with content as files (stage 1). Editorial maintenance without a developer then replaced that file layer with PocketBase, which revised AE-2 and AE-3 below — each now records both the original decision and why it changed.
AE-1: Blazor with static server-side rendering, not WebAssembly
Decision: Blazor Web App with interactivity mode None (pure static SSR). Target framework .NET 10 (LTS).
Rationale: Blazor WASM loads several MB of runtime before the first visible letter, serves search engines and link previews (WhatsApp, Signal, Messenger — the main distribution channel among parents) an empty shell, and provides no value on a pure information site. Static SSR delivers finished HTML, needs no JavaScript, and costs around 60 MB of RAM in the container.
Consequence: Interactivity can be added later per component
(@rendermode InteractiveServer on exactly that one component), without changing the
architecture. Never global.
AE-2: Content in PocketBase, read per request (revised)
Decision: All visible content lives in PocketBase (collections pages,
posts, events, faqs); the app reads it over the REST API per request via a
typed PocketBaseClient. Documents remain PDFs under wwwroot.
Original decision (stage 1): Content as files in the repo — Markdown with YAML
frontmatter, events as Content/events.yml. The rationale then was: no DB backup,
no migration schema, no file/DB drift, and "backup = git clone". For a handful of
pages that was the leanest option.
Why it changed: The site needs to be maintainable by the board without a developer and without a redeploy. Files meant every text fix was a commit and a rebuild. PocketBase keeps the "small, self-hostable, one binary" spirit while letting an editor change content live in its admin UI. It has no migration schema of the EF/ORM kind — the collections are its data — so the objection that weighed against "a database" in stage 1 does not apply here.
Consequence: Editing is now a task in the PocketBase admin, not a commit (see
redaktion.md). The app holds no content of its own; if PocketBase is unreachable a
page degrades to empty rather than failing. "Backup = git clone" no longer covers
the content — the backup is now pb_data (see AE-3 and deployment.md).
AE-3: Content lives in PocketBase's pb_data, not baked into the image (revised)
Decision: The editable content lives in PocketBase's pb_data, a persistent
volume separate from the app image. Only the static assets under wwwroot/ (CSS,
fonts, PDFs) are baked into the image.
Original decision (stage 1): Content/ and wwwroot/ were both baked into the
image, deliberately not a mounted volume, so the live state could never drift
from the repo and "backup = git clone" held.
Why it changed: Editing content live (AE-2) is only possible if that content survives a redeploy — so it must be a volume, not part of the image. The drift concern is answered differently now: the content simply has one home (PocketBase), and the repo no longer claims to hold it. The app image stays stateless and can be rebuilt and redeployed at any time without touching the data.
Consequence: The backup is a copy of pb_data (plus a restore test), not a
git clone (set up in issue #9, see deployment.md). The app reads content per
request over HTTP, so there is nothing cached at startup and no FileSystemWatcher
— an editorial change is live immediately, no restart needed.
AE-4: TLS and certificates exclusively in the Nginx Proxy Manager
Decision: The container speaks only HTTP on port 8080 and has no port mapping to the outside. NPM terminates TLS and is the only path to the container.
Rationale: Matches the pattern already established in the home lab
(cloud.anticarnist.de). Certificate management stays in one place.
Consequence (important): In Program.cs, no UseHttpsRedirection() and
no UseHsts() — otherwise a redirect loop behind the proxy. NPM sets HSTS.
UseForwardedHeaders with emptied KnownNetworks/KnownProxies is correct because
the container has no port mapping and is reachable only through NPM.
Rejected alternatives (for AE-1)
| Alternative | Why not |
|---|---|
| Blazor WASM | Payload, SEO, link previews, no benefit |
| ASP.NET Core MVC/Razor Pages | Works too, but Razor Components are the more modern model |
| Statiq.Web (C# SSG) → nginx | Leanest to operate, but every text change forces a build. Kept as a fallback. |
| Astro/Hugo | More mature SSG ecosystem, but unfamiliar territory |