Demo content from demo/developer-guide (0c17989)

This commit is contained in:
f451-admin 2026-09-29 18:52:55 +02:00
parent e7b15ff117
commit 4e32950ceb
12 changed files with 676 additions and 2 deletions

9
.order Normal file
View file

@ -0,0 +1,9 @@
architecture
principles
local-setup
repository-layout
api
web-frontend
markdown-and-editor
extending
testing-and-contributing

View file

@ -1,2 +0,0 @@
# developer-guide

77
api/index.md Normal file
View file

@ -0,0 +1,77 @@
---
id: api
title: The API
description: Route groups, authentication, and the review workflow's SHA contract.
tags: [api, architecture]
lang: en
---
# The API
`apps/api` is a Fastify server. It is the only part of f451 that checks
permissions, runs the review workflow and writes to the index — see
[[principles]]. Everything below is registered in `src/app.ts`.
## Route groups
| Group | Examples | Notes |
|---|---|---|
| System | `GET /healthz`, `GET /readyz` | Unauthenticated, for orchestrators. |
| Docs | `GET /api/docs`, `GET /api/openapi.json` | Swagger UI and the generated OpenAPI spec, open without a session. |
| Auth | `/auth/*` (OIDC login/callback/logout), `/auth/connect/*` (link a provider account) | Open — a login flow can't require a session to start one. |
| Session & tokens | `GET /api/me`, `/api/tokens/*` | Personal API token management for agents (see [[extending]]). |
| Read | `GET /api/spaces/:space/*` (pages, tree, search, broken links, graph, metadata schema, templates), `GET /api/pages/:id`, `GET /api/pages/:id/versions*` | Filtered by the caller's read access to the underlying repository. |
| Write / drafts | `POST /api/pages`, `.../draft`, `.../draft/media`, `.../locks`, `.../move`, `.../unarchive`, `DELETE /api/pages/:id`, `PUT /api/spaces/:space/order` | Require a linked provider account with write access; committed under the user's own token. |
| Review workflow | `/api/pages/:id/draft`, `/api/pages/:id/review`, `/api/pages/:id/review/request-changes`, `/api/pages/:id/release` (MCP tools `request_review` / `release_page`, see [[extending]]) | Turns a draft branch into a pull request, then merges it. |
| Media | `/media/*` | Binary asset delivery, gated like page reads. |
| Webhooks | `/webhooks/*` | Provider push events trigger incremental reindexing; HMAC-verified, no session. |
| Admin | `/admin/reindex`, `/admin/status`, `/admin/backfill-ids` | Bearer-token gated (`F451_ADMIN_TOKEN`), fail-closed if the token isn't configured. |
## Authentication
Two independent mechanisms protect `/api/*`, `/admin/*` and `/media/*`:
- **Session cookies**, established via OIDC login (Entra, Forgejo, or any
other OIDC provider). This is how the browser UI authenticates.
- **Personal API tokens** (`f451_pat_…`), issued through `/api/tokens` and
used by the MCP service and other automation. Tokens carry a scope —
`read` or `write` — and a read-scoped token gets a `403` on anything but
`GET`.
`/admin/*` additionally accepts a separate bearer token
(`F451_ADMIN_TOKEN`) instead of a session, so ops automation can call
`POST /admin/reindex` from a plain script.
A CSRF origin check runs on every state-changing request (`POST`, `PUT`,
`PATCH`, `DELETE`): a present-but-foreign `Origin` header is rejected. A
missing header is allowed through deliberately — that's the shape of
legitimate non-browser clients (CLI, CI, webhook senders), which
`SameSite=Lax` cookies don't protect against by themselves.
## `404` is deliberately ambiguous
Read routes never distinguish "this page doesn't exist" from "you don't
have access to it" — both are a plain `404`. See [[principles]] for why.
## The review workflow and its SHA contract
Nothing writes to the published version directly. The sequence is always:
1. **Create a draft** — a branch, from the current published state.
2. **Write** — save content to the draft branch, repeatedly.
3. **Request review** — opens a pull request.
4. **Release** — merges the pull request.
5. **Reindex** — the merge webhook (or a manual `/admin/reindex`) updates
the Postgres index from the new Git state.
Every write to a draft names a `baseSha`: the commit SHA the writer started
from. If the draft has moved on since — someone else saved a change, or a
previous save from the same writer landed — the API responds `409` with
the *current* SHA and content instead of silently overwriting it. The
caller is expected to re-fetch, reconcile, and retry with the new
`baseSha`.
> [!IMPORTANT]
> This is the same mechanism whether the writer is a human in the browser
> editor or an AI agent calling `update_page_draft` through MCP — see
> [[extending]]. There is no privileged, SHA-check-free path for agents.

49
architecture/index.md Normal file
View file

@ -0,0 +1,49 @@
---
id: architecture
title: Architecture
description: The three applications and four packages, and how a request travels through them.
tags: [architecture, overview]
lang: en
---
# Architecture
f451 is a pnpm monorepo on Node 22. Three applications and four shared
packages divide the work cleanly along one line: **`apps/api` is the only
place that checks permissions, runs the review workflow and writes to the
index.** Everything else either renders what the API gives it, or talks to
the API on someone's behalf.
## Request flow
```
Browser ──► apps/web (Next.js) ──► apps/api (Fastify) ──► Forgejo / GitHub
│ (content, permissions)
└──► PostgreSQL (index, sessions)
AI agent ──► apps/mcp (MCP↔HTTP) ──────────► apps/api (same HTTP API)
```
Nothing skips `apps/api`. The web app never talks to Git directly, and the
MCP service never talks to Postgres or Git directly — both reach content
and permissions exclusively through the API's HTTP surface.
## The three applications
| App | Role |
|---|---|
| `apps/api` (Fastify) | Indexing, read and write endpoints, webhooks, drift reconciliation, admin. The only place with permission checks, the review workflow and the index. |
| `apps/web` (Next.js App Router) | The user interface. Proxies `/api`, `/auth`, `/admin`, `/media` to the API, `/drawio` to the draw.io container, `/mcp` to the MCP service. Server Components by default; client islands stay small. |
| `apps/mcp` | Access for AI agents: a stateless translator between MCP and the HTTP API. It wraps only the HTTP API, holds no secret of its own, and passes the calling user's token through on every request. |
## The four packages
| Package | Role |
|---|---|
| `packages/markdown` | Renders Markdown to sanitized HTML. Shared by reading and writing so both sides agree on what a construct means. |
| `packages/editor` | The ProseMirror schema behind the WYSIWYG editor. Round-trip tests with `packages/markdown` keep reading and editing aligned — a document that survives Markdown → editor → Markdown unchanged. |
| `packages/design-tokens` | The catalog of design tokens that drives the look and feel — see [[web-frontend]]. |
| `packages/git-provider` | One interface, two implementations (Forgejo, GitHub): read/write files, commits, branches, pull requests, reviews. |
See [[principles]] for why the lines are drawn exactly here, and
[[repository-layout]] for the directories underneath each of these.

103
extending/index.md Normal file
View file

@ -0,0 +1,103 @@
---
id: extending
title: Extending f451
description: Recipes for the most common kinds of change — an API route, a UI string, a design token, a Markdown construct, an MCP tool, a Git provider.
tags: [howto, architecture]
lang: en
---
# Extending f451
Six recipes for changes that come up repeatedly. Each one names where the
change starts and what it touches on the way.
## Add an API route
1. Add a `register*Routes` function under `apps/api/src/routes/`, following
the existing pattern: it takes `{ db, spaces, providerRegistry, access,
canWrite, getUserProvider }` as needed, not the raw request context.
2. Register it in `apps/api/src/app.ts`, in the block that only runs when
`db && opts.spaces && opts.providerRegistry` are present — read-only
routes need `access`; anything that writes needs `canWrite` and
`getUserProvider` as well, since writes commit under the calling user's
own provider token (see [[principles]]).
3. Decide read or write semantics up front: read routes are filtered by
`access.canRead`, write routes additionally need a linked provider
account and go through the draft/SHA-contract machinery described in
[[api]] — don't bypass it for a "simple" new route.
4. Add a Swagger/JSON schema for the response; the OpenAPI spec at
`/api/openapi.json` is generated from it.
## Add a UI string
Add the key to both `apps/web/lib/i18n/messages/en/` and the matching
`de/` file in the same change — f451's interface is bilingual, and a
string that exists in only one language is a regression, not a partial
feature. See [[web-frontend]].
## Add a design token
1. Add the value to `packages/design-tokens/src/tokens.ts`.
2. Describe it in `catalog.ts`: its level (`theme` / `derived` /
`structure` / `switch`), its settings-page group, its role in plain
language, and whether a user theme is allowed to set it (`settable`,
with a `lockReason` if not).
3. Update the token-count tests for the affected level and group — they
are pinned deliberately, so the update should be a conscious edit, not
a number you copy without reading why it existed. See [[web-frontend]].
## Add a Markdown construct
Full recipe in [[markdown-and-editor]]. In short: parse and render it in
`packages/markdown`, make `stringify.ts` reproduce the same syntax, add the
matching node to `packages/editor` and its two conversion directions, then
add a round-trip test that proves Markdown → editor → Markdown is
lossless.
## Add an MCP tool
MCP tools in `apps/mcp/src/tools/` are thin wrappers: each one calls the
HTTP API (`apps/mcp/src/client.ts`) with the token passed through from the
calling agent, and does not hold state or a secret of its own. The
existing set is a useful map of scope:
- **Read** (`tools/read.ts`): `search_wiki`, `list_spaces`, `get_tree`,
`read_page`, `get_page_source`, `get_graph`, `list_broken_links`.
- **Write** (`tools/write.ts`): `create_page`, `edit_page`,
`update_page_draft`, `discard_page_draft`, `request_review`,
`release_page`, `request_changes` — the review workflow from [[api]],
exposed one step at a time.
- **Attachments** (`tools/attachments.ts`): `save_diagram`, `attach_file`.
A new tool follows the same shape: register it with `server.registerTool`,
give it a clear `title` and input schema, and implement it by calling an
existing API route — never by reaching around the API into the database or
Git directly. If no suitable API route exists yet, add one first (see
above); the MCP service is not the place to grow API-shaped logic of its
own.
## Add a Git provider
`packages/git-provider/src/types.ts` defines one interface,
`GitProvider` — file reads (`readFile`, `readFileBinary`, `listTree`),
writes (`writeFile`, `writeFileBinary`, `deleteFile`, the batched
`commitFiles`), branches, and the pull request lifecycle
(`createPullRequest`, `requestReviewers`, `submitPullRequestReview`,
`mergePullRequest`). `forgejo.ts` and `github.ts` are the two existing
implementations.
A third provider means a new file implementing every method of
`GitProvider`, throwing the same three error types
(`NotFoundError`/`ConflictError`/`ProviderError`) the existing
implementations use — callers in `apps/api` branch on those errors, not on
provider-specific ones. `commitFiles` is worth reading closely before
implementing it: it exists because writing many files one call at a time
was measured to take minutes on a large move, and its batching behavior
(splitting large change sets across multiple underlying commits, returning
the last commit's SHA, treating an empty list as a no-op) is part of the
contract, not an implementation detail.
> [!TIP]
> None of these recipes need a browser or a full test run to make progress
> on. See [[testing-and-contributing]] for what to actually check before
> opening a pull request.

46
index.md Normal file
View file

@ -0,0 +1,46 @@
---
id: overview
title: Developer Guide
description: How to understand, run and extend f451, the git-native documentation wiki.
tags: [overview, getting-started]
lang: en
---
# f451 Developer Guide
f451 is a wiki whose content lives entirely in Git. Every space is a
repository on Forgejo or GitHub, every page a Markdown file with YAML
frontmatter. PostgreSQL only holds a derived index — search vectors,
rendered HTML, the knowledge-graph edges — and can always be rebuilt from
Git. This demo space is for developers who want to run f451 locally,
understand how the pieces fit together, and add something to it.
> [!NOTE]
> This is a demo space, not the project's own documentation. It exists to
> show what a well-structured f451 space looks like while explaining f451
> itself.
## Where to start
- [[principles]] — the four architectural rules that shape every decision
in the codebase, and why they exist
- [[architecture]] — the three applications and four packages, and how a
request travels through them
- [[local-setup]] — get a working stack on your machine
- [[repository-layout]] — what lives where in the monorepo
## Going deeper
- [[api]] — route groups, authentication, the review workflow and its SHA
contract
- [[web-frontend]] — routing, the proxy, internationalization, styling and
design tokens
- [[markdown-and-editor]] — the Markdown pipeline and the editor that has
to stay in sync with it
## Contributing
- [[extending]] — recipes for the six most common kinds of change
- [[testing-and-contributing]] — what to run before you open a pull
request, what CI runs, and the license terms your contribution falls
under

84
local-setup/index.md Normal file
View file

@ -0,0 +1,84 @@
---
id: local-setup
title: Local setup
description: Get a working f451 stack on your machine, and the commands you'll use daily.
tags: [setup, getting-started]
lang: en
---
# Local setup
f451 needs Node 22 and pnpm 9, plus Docker (or Podman) for the two stacks
it runs locally: a Forgejo instance for content and sign-in, and the wiki
itself.
> [!NOTE]
> This mirrors the top-level `README.md`. If the two ever disagree, the
> README is authoritative — this page exists so the setup steps sit next
> to the rest of the developer documentation.
## Why not `localhost`
The browser and the containers must reach Forgejo and the wiki under the
*same* address, because inside a container `localhost` means the container
itself, not your machine. Pick your machine's LAN address once and use it
everywhere below (called `<IP>`).
## Steps
1. **Start Forgejo first.** It must be running before the wiki stack, since
the wiki authenticates against it.
```
cat > deploy/git/docker-compose.override.yml <<EOF
services:
forgejo:
environment:
FORGEJO__server__ROOT_URL: http://<IP>:3300/
EOF
docker compose -f deploy/git/docker-compose.yml up -d
```
2. **Run the setup script.** It creates an admin account, an organization,
an OAuth app and a demo space, and writes `deploy/wiki/.env`:
```
FORGEJO_URL=http://<IP>:3300 WEB_BASE=http://<IP>:8080 ./scripts/dev-local-setup.sh
```
3. **Start the wiki:**
```
cd deploy/wiki && docker compose up -d --build
```
4. **Open** `http://<IP>:8080`, sign in through Forgejo
(`wiki-admin` / `admin1234`), then link your Forgejo account under
*Settings → Connections*. Spaces only become visible and writable once
your account is linked — this is the "permissions come from the Git
provider" rule from [[principles]] in practice.
> [!WARNING]
> This setup is for local development only: plain HTTP, insecure cookies.
> It is not meant to be exposed beyond your machine.
Database migrations do not run automatically after an update; run
`docker compose run --rm api node dist/db/migrate-cli.js`.
## Everyday commands
Once the stack is up, most development happens outside Docker:
| Purpose | Command |
|---|---|
| Type-check every package | `pnpm -w typecheck` |
| Run every test | `pnpm -w test` |
| Run one test file | `pnpm --filter @f451/web test -- lib/urls.test.ts` |
| API in dev mode (port 3001) | `pnpm --filter @f451/api dev` |
| Web in dev mode (port 3000) | `pnpm --filter @f451/web dev` |
| Generate a database migration | `pnpm --filter @f451/api db:generate` |
| CSS metrics with bounds | `pnpm css:inventar` |
For what to actually run before opening a pull request, see
[[testing-and-contributing]] — it is a much shorter list than "everything
above".

View file

@ -0,0 +1,76 @@
---
id: markdown-and-editor
title: Markdown and the editor
description: The Markdown pipeline, its constructs, and adding a construct in both packages.
tags: [markdown, editor, architecture]
lang: en
---
# Markdown and the editor
Two packages share the job of understanding a page's content:
`packages/markdown` turns Markdown into sanitized HTML for reading;
`packages/editor` provides the ProseMirror schema behind the WYSIWYG
editor. They have to agree, because a page can be opened in either mode at
any time and must come back unchanged.
## The Markdown pipeline
`packages/markdown/src/`:
- `parse.ts` / `render.ts` / `stringify.ts` — Markdown → AST → HTML, and
back to Markdown text.
- `frontmatter.ts` / `frontmatter-split.ts` / `frontmatter-metadata.ts` —
the YAML header every page carries.
- `alerts.ts` — the `> [!NOTE]` / `[!TIP]` / `[!IMPORTANT]` / `[!WARNING]`
callouts used throughout this space.
- `image-size.ts`, `youtube.ts` — extra constructs beyond plain CommonMark.
- `slug.ts`, `diff.ts`, `version.ts` — heading slugs, and support for page
version comparison.
## The editor
`packages/editor/src/`:
- `extensions.ts` — assembles the ProseMirror schema.
- `nodes/alert.ts`, `nodes/image.ts`, `nodes/wiki-link.ts`,
`nodes/youtube-embed.ts` — one node per construct that needs editor
support beyond stock ProseMirror.
- `from-markdown.ts` / `to-markdown.ts` — the two directions of
conversion between editor document and Markdown text.
## Constructs shared by both packages
Wikilinks (`[[page-id]]`, `[[page-id|label]]`) and callouts are the two
constructs every page in this space uses, and both need a matching node in
`packages/editor` (`wiki-link.ts`, `alert.ts`) so that opening a page in
the WYSIWYG editor round-trips it correctly.
## Adding a construct to both packages
A new Markdown construct (say, a new kind of callout, or a new inline
element) needs changes in four places, in this order:
1. **`packages/markdown`** — teach the parser to recognize the syntax and
the renderer to produce sanitized HTML for it.
2. **`packages/markdown`** — teach `stringify.ts` to produce the same
syntax back out, so a document that was never touched in the editor is
byte-for-byte stable.
3. **`packages/editor`** — add a node (or mark) under `nodes/` and wire it
into `extensions.ts`, then extend `from-markdown.ts` and
`to-markdown.ts` for the two conversion directions.
4. **Round-trip test** — Markdown → editor document → Markdown must
reproduce the original. This is the test that actually catches drift
between the two packages; skipping it is how "reading shows X, editing
shows Y" bugs get in.
> [!WARNING]
> If the sanitizer (used by `packages/markdown`'s HTML output) doesn't know
> about the new construct's tags or attributes, it will silently strip
> them. This is exactly the failure mode the diagram sanitizer test guards
> against for `.drawio.svg`/`.excalidraw.svg` — the `content` attribute and
> wrapping text elements must survive sanitization, or diagrams lose their
> source and their full labels.
See [[extending]] for the same recipe phrased as a short checklist, and
[[api]] for how the rendered result reaches the reader.

65
principles/index.md Normal file
View file

@ -0,0 +1,65 @@
---
id: principles
title: Principles
description: The four rules that shape f451's design, and why they exist.
tags: [architecture, principles]
lang: en
---
# Principles
Four rules recur throughout the codebase. They are not style preferences —
each one closes off a class of bugs or design mistakes that the project has
already made once.
## Git is the source of truth, Postgres is only an index
Every space is a Git repository; every page is a Markdown file with
frontmatter. Postgres holds the rendered content, the search vector, the
knowledge-graph edges, sessions and encrypted provider tokens — nothing
that can be derived from Git is allowed to live *only* in the database.
> [!IMPORTANT]
> Losing `pg-data` is recoverable: `POST /admin/reindex` or the drift job
> rebuilds the index from Git. Losing `forgejo-data` is not — that is the
> one thing backups exist for. If you find yourself adding a column that
> holds information not derivable from the Git content, stop and ask
> whether it belongs in Git instead.
## Permissions come from the Git provider
f451 has no role system of its own. Whether someone can see a space or
write to it is decided by their permissions on the underlying repository.
Writers always write with **their own** provider token — the API never
commits under a service account. Without a linked provider account, every
write attempt fails with 403; that is a precondition, not a bug.
This also shapes how "not found" behaves: a `404` deliberately means
*"doesn't exist, or you don't have access"* — never distinguish the two.
Doing so would let an unauthorized caller learn that a page exists, which
is exactly the kind of existence oracle this design avoids.
## Writing always goes through the review workflow
There is no route that writes directly to the published version — not for
people, not for agents. The path is always: create a draft (a branch) →
write → open a review (a pull request) → approve (merge) → reindex.
Draft routes carry a SHA contract: whoever writes names the `baseSha` of
the version they started from. If the content has changed underneath them,
they get a `409` back with the current state instead of a silent
overwrite. See [[api]] for the routes that implement this.
## Diagrams carry their own source
`.drawio.svg` and `.excalidraw.svg` files under `<page folder>/_media/` are
plain SVGs that also carry the diagram's source XML in a `content`
attribute. One file is both the picture people see and the thing the
diagram editor reopens. The SVG sanitizer keeps that attribute and the
wrapping text elements deliberately — stripping either would make the
diagram unreadable or unopenable.
> [!TIP]
> These four rules are the first thing to check when a design choice looks
> surprising: it is very likely a direct consequence of one of them, not an
> arbitrary decision.

View file

@ -0,0 +1,44 @@
---
id: repository-layout
title: Repository layout
description: What lives where in the monorepo.
tags: [architecture, reference]
lang: en
---
# Repository layout
A pnpm workspace with three applications and four packages, plus
deployment and operational tooling around them.
## `apps/`
| Directory | Contents |
|---|---|
| `apps/api` | Fastify server: route registration in `src/app.ts`, routes under `src/routes/`, the indexer under `src/indexer/`, draft/workflow logic under `src/drafts/`, auth under `src/auth/`. See [[api]]. |
| `apps/web` | Next.js App Router UI. Proxy configuration in `next.config.ts`, styles in `app/styles/*.css`, i18n messages in `lib/i18n/messages/`. See [[web-frontend]]. |
| `apps/mcp` | The stateless MCP↔HTTP translator. Tools live under `src/tools/` (`read.ts`, `write.ts`, `attachments.ts`), diagram generation under `src/diagram/`. |
## `packages/`
| Directory | Contents |
|---|---|
| `packages/markdown` | Parsing, rendering and stringifying Markdown (`parse.ts`, `render.ts`, `stringify.ts`), plus the shared constructs: `alerts.ts` (callouts), `frontmatter.ts`, `youtube.ts`, `image-size.ts`, `slug.ts`, `diff.ts`. |
| `packages/editor` | The ProseMirror schema: `extensions.ts`, `nodes/` (`alert.ts`, `image.ts`, `wiki-link.ts`, `youtube-embed.ts`), and the two directions of conversion, `from-markdown.ts` / `to-markdown.ts`. |
| `packages/design-tokens` | `tokens.ts` (generated values) and `catalog.ts` (one entry per token: level, settings-page group, role, whether a user theme may override it). |
| `packages/git-provider` | The `GitProvider` interface (`types.ts`) and its two implementations, `forgejo.ts` and `github.ts`. |
## Everything else
| Directory | Contents |
|---|---|
| `deploy/git` | Forgejo stack — must be started before `deploy/wiki`. |
| `deploy/wiki` | The wiki stack (web, api, drawio, mcp, Postgres). |
| `docs/` | Domain vocabulary, architecture decisions, agent-facing docs, design specs. |
| `scripts/` | Local setup (`dev-local-setup.sh`), deployment E2E, CSS metrics, seed content for the local demo space. |
| `.github/workflows/ci.yml` | The CI pipeline — see [[testing-and-contributing]]. |
> [!TIP]
> When you're not sure which package owns a piece of behavior, start from
> [[architecture]]'s table of roles rather than guessing from file names —
> the boundaries are drawn by responsibility, not by technology.

View file

@ -0,0 +1,64 @@
---
id: testing-and-contributing
title: Testing and contributing
description: Targeted tests over full runs, what CI checks, and the license your contribution falls under.
tags: [testing, contributing, license]
lang: en
---
# Testing and contributing
## Run targeted checks, not the full suite
For a given change, run type-checking across the workspace and only the
test files of the modules you touched:
```
pnpm -w typecheck
pnpm --filter @f451/web test -- lib/urls.test.ts
```
Not the full `pnpm -w test` for every change — its output is the actual
cost driver on a change of any size, and it re-proves things your change
didn't touch. Full-suite runs and browser/E2E verification are what CI and
review are for, not every local iteration.
## What CI checks
`.github/workflows/ci.yml` runs on every push to `main`/`dev` and on every
pull request:
```
pnpm install --frozen-lockfile
pnpm typecheck
pnpm test
pnpm build
```
Two further jobs exist but only run on manual dispatch, because they are
expensive: a GitHub live-contract test against `packages/git-provider`
(needs real credentials), and a Playwright end-to-end read-flow test
(needs a browser install and container startup).
## Before opening a pull request
- Type-check, and run the tests for the files you changed (above).
- Keep the change to what the task asked for; unrelated cleanup belongs in
its own pull request.
- New code, comments, commit messages and documentation are written in
English. f451's UI itself stays bilingual — see [[web-frontend]] — but
written material *about* the project is English going forward.
## License
f451 is source-available, not Open Source in the OSI sense: noncommercial
use, modification and redistribution are free, with attribution to rote
code fraktion and a link to the project, both in the code and in the
running application's interface. Commercial use requires a separate
license on request. Full terms, including named exclusions, are in
`LICENSE.md` at the repository root — read it before assuming a use case
is covered.
> [!NOTE]
> Contribution guidelines beyond this page live in `CONTRIBUTING.md` at the
> repository root.

59
web-frontend/index.md Normal file
View file

@ -0,0 +1,59 @@
---
id: web-frontend
title: The web frontend
description: Routing, the proxy, internationalization, and styling with design tokens.
tags: [frontend, architecture]
lang: en
---
# The web frontend
`apps/web` is a Next.js App Router application. Server Components are the
default; client islands are kept as small as the interaction requires
(theme toggle, search dialog, tree expansion are the reference examples).
## The proxy
`apps/web` is not a client of the API in the usual sense — it *proxies*
several path prefixes straight through, configured in `next.config.ts`:
| Prefix | Destination |
|---|---|
| `/api/:path*` | `apps/api` |
| `/auth/:path*` | `apps/api` |
| `/admin/:path*` | `apps/api` |
| `/media/:path*` | `apps/api` |
| `/drawio/:path*` | the draw.io container |
| `/mcp` | the MCP service |
Because these are rewrites, not client-side fetches, requests to them carry
the API's own response headers (its `onSend` security hook, or the media
route's sandboxed CSP) rather than anything `apps/web` adds — `headers()`
in `next.config.ts` only applies to responses Next.js generates itself.
## Internationalization
The interface is bilingual (German/English). UI strings live under
`apps/web/lib/i18n/messages/{de,en}/`. This is separate from page content
language: a page's own `lang` frontmatter field (as in this space, `en`)
describes the content, not the chrome around it.
## Styling and design tokens
`apps/web/app/globals.css` contains no rules of its own — only the list of
imports from `app/styles/*.css`. **That import order is the cascade**; the
numeric prefixes in the filenames are not what determines it.
The token values themselves are generated, not hand-maintained: `sync-tokens`
runs automatically as `predev`/`prebuild` and produces the CSS variables
from `packages/design-tokens`. See [[markdown-and-editor]] for the
package's counterpart on the content side, and [[extending]] for the steps
to add a token.
> [!NOTE]
> `packages/design-tokens/src/catalog.ts` separates *values* (`tokens.ts`)
> from *description* (`catalog.ts`: which settings-page group a token
> belongs to, its role in plain language, and whether a user theme is
> allowed to override it). Tests pin the count of tokens per level and
> group, so adding one means updating those counts deliberately, not
> incidentally.