How it works¶
freundebuch is one Next.js app with a SQLite database. There is no sidecar: the MCP endpoint, the REST API and the web pages all call the same framework-free domain layer in backend/.
flowchart LR
claude["Claude app<br/>(voice ramble)"] -- "MCP, OAuth token" --> mcp["/mcp"]
phone["Browser"] -- "cookie" --> pages["Pages"]
client["Scripts"] -- "OAuth token" --> rest["/api/v1"]
mcp --> domain["Domain layer<br/>backend/"]
pages --> domain
rest --> domain
domain --> db[("SQLite<br/>/data/freundebuch.db")]
domain -- "daily" --> hook["Webhook (ntfy)"]
authentik["authentik"] -. "OIDC sign-in" .- pages
Sign-in¶
- Pages use a sign-in cookie set after the authentik (OIDC) sign-in.
- Claude connects through freundebuch's own OAuth server: dynamic client registration, PKCE, and the authorize step is that same authentik sign-in.
- Tokens are stateless HMAC-signed values tied to one surface: an MCP token doesn't work on REST, a REST token doesn't work on MCP, and the browser cookie never works on MCP.
Privacy¶
Every row a person can see is either shared or recorded by them. That single rule is one function in the query layer, and every read path goes through it, including counts, hints, the graph and notifications. ADR 0002 explains the details.
Notifications¶
A timer in the server process checks once per minute whether it's past FREUNDEBUCH_NOTIFY_AT. Then it sends each person their own due list to their webhook. Progress is tracked per person, so one broken webhook doesn't block or repeat anyone else's reminders.