Files
MindSift/Doku/architecture.md
T
sandrodachandClaude Opus 5.5 4cdbcbd6b5 fix(security): never sign tokens with a known JWT secret (FLAW-04)
Without JWT_SECRET, or with one of the published placeholders, the server
generates 32 random bytes once and keeps them in the data directory
(jwt.secret, mode 0600), so tokens cannot be forged and logins survive
restarts. Instances that ran on the old default log in once more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 07:35:45 +01:00

9.2 KiB

Architektur

Überblick

Browser (React SPA, PWA-Manifest)
   │  REST /api/v1/*           Authorization: Bearer <JWT | mindsift_sec_…>
   │  WebSocket /api/v1/ws     Fallback: SSE /api/v1/events
   ▼
Go-Server (chi)  ── internal/api   Handler, Middleware, Alarm-Ticker
   │                internal/ws    Hub (Räume, Benutzer-Kanäle, SSE)
   │                internal/auth  JWT, bcrypt, API-Key-Hash
   ▼                internal/config
SQLite (modernc.org/sqlite, WAL)  ── internal/storage

Das Frontend liegt per go:embed im Binary (cmd/server/main.go). Unbekannte Pfade liefern index.html (SPA-Routing); index.html, sw.js und manifest.json werden mit no-store ausgeliefert.

Stack

Schicht Technik
Backend Go 1.23, go-chi/chi v5, go-chi/cors, gorilla/websocket, golang-jwt/jwt v5, x/crypto/bcrypt
Datenbank SQLite über modernc.org/sqlite (reines Go, kein CGO), WAL, foreign_keys=ON
Frontend React 18, TypeScript, Vite 5, Tailwind 3, framer-motion (Triage-Karten), lucide-react, prismjs (Code-Blöcke)
Auslieferung Mehrstufiges Dockerfile (Node 22 → Go 1.23 → Alpine 3.21), Image git.dsit.me/sandrodach/mindsift

Verzeichnisse

Pfad Inhalt
cmd/server/ main.go, eingebettetes dist/
internal/api/ server.go (Routen, Handler, Alarm-Ticker, SMTP/Discord/Webhook-Versand)
internal/storage/ sqlite.go (Schema, Migrationen, gesamte Datenlogik)
internal/domain/ Strukturen (models.go), ID-Erzeugung, Tag-Extraktion
internal/ws/ hub.go
internal/collab/ Kern der Notiz-Synchronisation ohne Datenbank: Blockbaum, Operationen und Konfliktregeln, Sortierschlüssel, Leases, Long-Poll-Benachrichtigung
internal/auth/ Tokens und Passwörter
internal/config/ Umgebungsvariablen
frontend/src/features/ triage, workspace, lists, teams, settings, capture, auth, help
frontend/src/lib/ api.ts (REST-Client, Transport-Umschaltung WS/SSE), types.ts, markdown.tsx, theme.ts
frontend/src/lib/docSync/ Notiz-Synchronisation im Browser: Portierung von internal/collab (model.ts, orderKey.ts), Vergleich Editor-Liste → Operationen (diff.ts), engine.ts, IndexedDB (persist.ts), HTTP (transport.ts)
e2e/ Browser-Tests mit Playwright gegen einen echten Server (e2e/run.sh)
frontend/src/themes/ CSS pro Theme

Start und Laufzeit

  1. config.Load() liest die Umgebung (siehe unten).
  2. storage.NewStore() öffnet SQLite (busy_timeout, WAL, max. 8 Verbindungen) und führt migrate() aus.
  3. SMTP-Werte aus der Umgebung werden in system_settings übernommen, sofern dort noch nichts steht.
  4. Mit Demo-Flag legt EnsureDemoAccounts die Konten admin/admin und user/user an.
  5. api.NewServer startet den Alarm-Ticker (jede Minute), die Verdichtung des Notiz-Logs (beim Start, dann alle 6 Stunden) und baut den Router. Server-Timeouts: Lesen 15 s, Schreiben 30 s, Leerlauf 60 s. Beim Beenden wird sauber heruntergefahren.

Konfiguration

Variable Standard Bedeutung
PORT 8080 HTTP-Port
DATA_DIR ./data (Container: /data) Verzeichnis der Datenbank
DATABASE_URL leer sqlite:///pfad/datei.db oder Pfad; ohne Angabe DATA_DIR/mindsift.db. Postgres-URLs werden nicht unterstützt (siehe Hinweis)
JWT_SECRET zufällig erzeugt, gespeichert in $DATA_DIR/jwt.secret Signaturschlüssel der Tokens; bekannte Platzhalter werden ignoriert (FLAW-04)
BASE_URL http://localhost:<PORT> Basis-URL der Instanz
ALLOW_REGISTRATION false Startwert; die Einstellung im Admin-UI (system_settings) hat Vorrang
LOG_LEVEL / DEBUG info / false debug, info, warn, error; DEBUG=true erzwingt debug
MINDA_DEMO (auch MINDSIFT_DEMO, DEMO_MODE) false Demo-Konten anlegen
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM, SMTP_TLS leer / 587 / true Mailversand, auch im Admin-UI einstellbar
SMTP_SKIP_VERIFY false Zertifikat des Mailservers nicht prüfen (nur interne Server mit selbstsigniertem Zertifikat)
LEGAL_IMPRINT_URL, LEGAL_PRIVACY_URL leer Impressum/Datenschutz als externer Link (nur http(s))
LEGAL_IMPRINT_CONTENT, LEGAL_PRIVACY_CONTENT (oder …_CONTENT_FILE mit Dateipfad) leer Impressum/Datenschutz als Markdown, angezeigt in einem Lesefenster. Ohne URL und Text bleibt der Link unsichtbar.

Hinweis: DATABASE_URL mit postgres… führt dazu, dass der Standardpfad in DATA_DIR genutzt wird; Postgres ist nicht implementiert.

Authentifizierung

  • Login mit E-Mail, Benutzername (aus dem E-Mail-Präfix) oder login. Passwörter: bcrypt (DefaultCost).
  • JWT: HS256, Laufzeit 30 Tage, Claims userId, email, displayName, tokenVersion, jti. Gültig nur, wenn Token nicht in revoked_tokens steht, das Konto aktiv ist und tokenVersion mit users.token_version übereinstimmt. token_version wird bei Passwortänderung, Rollenwechsel, Deaktivierung und „Alle Sitzungen beenden“ erhöht.
  • Logout schreibt den Token-Hash in revoked_tokens.
  • API-Keys: mindsift_sec_<hex>, gespeichert als SHA-256, nur das Präfix bleibt sichtbar. Der Key gilt als Bearer-Token oder in X-API-Key und authentifiziert als Besitzer (FLAW-07).
  • Registrierung: nur mit Einladungscode oder wenn allow_registration aktiv ist. Das erste Konto entsteht über /auth/setup und wird Administrator.

Hintergrundjob: Alarme

startAlarmTicker prüft jede Minute alle offenen, noch nicht gemeldeten Aufgaben mit Fälligkeit. Ist due_date <= heute (Serverzeit), sendet er über die Kanäle des Benutzers (email, discord, webhook) und setzt alarm_sent, sobald mindestens ein Kanal erfolgreich war. Details und Einschränkungen: modules/tasks.md, FLAW-12.

Hintergrundjob: Verdichtung der Notiz-Synchronisation

startCompaction räumt beim Start und dann alle 6 Stunden auf: abgelaufene Leases und Sitzungen im Speicher, Log-Einträge älter als 30 Tage (private Notizen: 7 Tage), Grabsteine älter als 30 Tage und Idempotenz-Ergebnisse älter als 90 Tage. Details: database.md.

Build

Stage 1  node:22-alpine   tsc, vitest, vite build            → frontend/dist
Stage 2  golang:1.23      go vet, go test, go build          → /app/mindsift (dist wird eingebettet)
Stage 3  alpine:3.21      nur Binary, ca-certificates, tzdata

Image-Tags (CI): Push auf dev → :dev; Push/Merge auf main → :latest; Git-Tag v* (z. B. v1.2.0) → :v1.2.0 und :stable. Andere Branches bauen nicht.

Die angezeigte Version (Kopf der App) setzt der Build-Parameter APP_VERSION: Die CI übergibt bei Git-Tags den Tag-Namen (v0.2), sonst zeigt die App die Version aus frontend/package.json mit -dev (v0.2-dev). Umgesetzt in frontend/vite.config.ts und frontend/src/version.ts.

Die Tests laufen beim Image-Build (Frontend: npm run test:run, Backend: go test -v ./...); ein Fehler bricht den Build ab. Frontend-Tests liegen unter frontend/src/**/__tests__/ (Vitest + jsdom + Testing Library, Setup in frontend/src/test/setup.ts).

App-Icons, PWA & Social-Preview (Open Graph)

MindSift verwendet plattformübergreifend ein einheitliches App-Icon-Design, das exakt dem Icon im Kopfbereich der mobilen App entspricht:

  • Design & Geometrie: Sanft abgerundetes Quadrat (rx=108 auf 512x512 Canvas bzw. rounded-xl), Primärblau (--accent-primary: #2563eb) als Hintergrund und zentriertes Lucide Brain Icon in reinem Weiß (--text-on-accent: #ffffff).

Asset-Matrix (frontend/public/)

Datei Zweck & Zielplattform Besonderheit
icon.svg Modernes Standard-Icon & SVG-Favicon Vektor-Original mit abgerundeten Ecken
icon-192x192.png, icon-512x512.png PWA-Installation (purpose: "any"), Desktop Linux/Windows Erforderlich für Chromium unter Linux (erzeugt sonst kein .desktop-Icon)
icon-maskable-192x192.png, icon-maskable-512x512.png, icon-maskable.svg Android Adaptive Icons (purpose: "maskable") Full-Bleed Hintergrund (#2563eb) ohne transparente Ecken, Brain im 80%-Sicherheitsbereich
apple-touch-icon.png (180x180) iOS Safari („Zum Home-Bildschirm“) Opaker Hintergrund verhindert schwarze Ränder beim automatischen Apple-Squircle-Maskieren
favicon.ico, favicon-32x32.png, favicon-16x16.png Browser-Tabs, Lesezeichen, Chronik Multi-Resolution-ICO (16/32/48) für alle Desktop-Browser
og-image.png (1200x630), og-image-square.png (512x512) Link-Vorschau in WhatsApp, Discord, Telegram, X/Twitter Dunkler #0b0f19-Hintergrund mit Icon & Claim; Datei-Größe < 120 KB (WhatsApp-Limit < 300 KB)

Dynamische OpenGraph-URL-Injektion

Messenger-Crawler (insbesondere WhatsApp) werten relative Bild-URLs in og:image oft nicht korrekt aus. Der Go-SPA-Handler in internal/api/server.go ersetzt beim Ausliefern von index.html automatisch relative Pfade (/og-image.png, og:url) durch die absolute Server-Origin (scheme://host), ermittelt über X-Forwarded-Proto und X-Forwarded-Host bzw. req.Host.