Skip to content

Architecture

flowchart LR
  subgraph Browser
    W[React + tldraw app]
    L[Landing page]
  end
  subgraph Server["Hetzner server (Docker Compose)"]
    C[Caddy<br/>HTTPS, static files]
    A[FastAPI]
    S[(PostgreSQL)]
    F[(uploads volume)]
  end
  W -- /api --> C --> A
  L --> C
  A --> S
  A --> F
  A -- vision + structured outputs --> Claude[Claude API]
  A -- reminders --> T[Telegram Bot API]
  A -- reminders --> R[Resend]

Data model

Table Purpose
users Teachers and students. Students belong to one teacher and carry reminder settings (language, Telegram chat ID, channels, next lesson time).
lessons One tutoring session: title, subject, time, tutor notes, AI summary and its status.
boards One whiteboard image of a lesson: the stored PNG, AI scene, tldraw snapshot and status.
reminders Log of every reminder attempt per channel. It is also the dedup key for scheduled reminders.

Tables are created on startup (Base.metadata.create_all). Alembic migrations are on the roadmap before real data goes into production.

Request flow: uploading a board

sequenceDiagram
  participant T as Teacher (web)
  participant A as API
  participant C as Claude
  T->>A: POST /api/lessons/{id}/boards (images / PDF)
  A->>A: normalize to PNG, one board per page
  A-->>T: 201 boards (status: pending)
  A->>C: image + system prompt, output_format=Scene
  C-->>A: validated Scene JSON
  A->>A: scale coordinates, status: ready
  T->>A: poll GET /api/lessons/{id}
  T->>A: GET /api/boards/{id} -> scene
  T->>T: build tldraw shapes, autosave snapshot

Background work runs in-process via FastAPI BackgroundTasks. Each job opens its own DB session and records failures on the row instead of raising. A dedicated queue (Redis + arq) is only worth it once a single server can't keep up.

Board rendering

Every board has a scene (api/app/scene.py): a list of elements with board-pixel coordinates. Scenes come from the draw.io importer (api/app/importers/drawio.py) or from the AI (api/app/ai/vision.py, converted with VisionScene.to_scene). web/src/board/sceneToShapes.ts turns a scene into tldraw shapes and then creates the arrow bindings that keep connectors attached to the elements they point at. See Importing boards for the element types.

After the first user edit, the full tldraw snapshot is stored and becomes the source of truth for that board. Re-running a board (AI or re-import) clears the snapshot and rebuilds it from the new scene.