Skip to content

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.

Design decisions