Files
MindSift/Doku/architecture.md
T
sandrodachandClaude Opus 5.5 11c14fa9ff
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
feat(auth): lock logins after repeated failures, trust proxies explicitly (FLAW-09)
Failed logins are counted per account+address (3), per account (20) and
per address (30) within 15 minutes; a full bucket answers 429 with
Retry-After for 15 minutes. Locking the account+address bucket first
means an attacker cannot lock the owner out from elsewhere. Unknown
names count like accounts. Success clears the account buckets, a
password reset (mail or admin) lifts the lock. LOGIN_PROTECTION=false
switches it off.

chi's RealIP believed X-Forwarded-For from anyone. The client address
now comes from the headers only when the connection is from a trusted
proxy: TRUSTED_PROXIES=private (default: loopback, Docker, LAN), all
(old behaviour), none, or CIDRs. A proxy that sends no client address
is logged once and per-address limits are skipped for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:26:02 +01:00

13 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 (Server, Router mit allen Routen, Middleware, JSON-Helfer); Handler nach Bereich in handlers_*.go (auth, account, thoughts, documents, folders, lists, tasklists, teams, admin, integrations), realtime.go (WebSocket/SSE), alarms.go (Ticker und Versand), docsync.go, mail.go, netguard.go, password_reset.go
internal/storage/ sqlite.go (Store, Schema, Migrationen); Datenlogik nach Bereich: users.go, apikeys.go, thoughts.go (inkl. Alarme), tasklists.go, documents.go, folders.go, tags.go, lists.go, teams.go, system.go (Einstellungen, Statistik, Export), dazu visibility.go, docsync.go, search.go, trash.go, backlinks.go, recurrence.go u. a.
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)
TRUSTED_PROXIES private Proxys, deren X-Forwarded-For/X-Real-IP geglaubt wird (private, all, none, CIDR-Liste); internal/api/clientip.go
LOGIN_PROTECTION true Login-Sperre nach Fehlversuchen (internal/api/loginguard.go), false = aus
BASE_URL http://localhost:<PORT> Basis-URL der Instanz; explizit gesetzt Voraussetzung für das Passwort-Zurücksetzen per Mail
TZ Europe/Berlin Zeitzone für Fälligkeiten und Alarme; die Zeitzonendaten sind ins Binary eingebettet (time/tzdata)
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
DEMO_RESET_MINUTES 20 Demo-Modus: periodischer Reset auf die Beispieldaten (0 = aus). Feste IDs der Demo-Konten (usr_demo_admin, usr_demo_user) halten Sitzungen gültig; laufende Anfragen sperren den Reset (internal/api/demo.go, Streams ausgenommen); danach DEMO_RESET an alle Verbindungen, /auth/status liefert demoNextReset/demoResetInSeconds.
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).
  • Login-Sperre: Fehlversuche zählen im Speicher in drei Töpfen: Konto + Adresse (3), Konto (20), Adresse (30), je innerhalb von 15 Minuten; ist einer voll, gibt es 15 Minuten lang 429 mit Retry-After. Unbekannte Namen zählen wie Konten (kein Erraten, welche existieren). Erfolg leert die Kontotöpfe, ein Passwort-Reset (Mail oder Admin) hebt die Sperre auf. Die Client-Adresse kommt nur von vertrauenswürdigen Proxys (TRUSTED_PROXIES).
  • 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. Neue Passwörter brauchen mindestens 8 Zeichen (auth.ValidatePassword). 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 darf nur Daten anliefern (POST /inbox, POST /webhooks/ingest); alle anderen Endpunkte antworten mit 403 (FLAW-07).
  • Passwort vergessen: nur wenn SMTP und BASE_URL gesetzt sind (nicht im Demo-Modus; passwordReset in /auth/status). POST /auth/forgot-password antwortet immer gleich und verschickt die Mail im Hintergrund, höchstens alle 2 Minuten pro Konto. Der Link BASE_URL/?resetToken=… gilt eine Stunde und einmal; gespeichert wird nur der SHA-256-Hash (password_reset_tokens). POST /auth/reset-password setzt das Passwort, entwertet alle offenen Links des Kontos und erhöht token_version (alle Sitzungen enden). Die App entfernt den Token sofort aus der Adresszeile.
  • 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 der Alarmzeitpunkt erreicht (Fälligkeit mit Uhrzeit bzw. 9:00 bei ganztägigen Aufgaben, minus Vorlaufzeit des Benutzers, Zeitzone TZ), sendet er über die Kanäle des Benutzers (email, discord, webhook) und setzt alarm_sent, sobald mindestens ein Kanal erfolgreich war. Nach 3 Durchläufen ohne Erfolg gibt er auf (alarm_failures). 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).

Offline (PWA)

  • Service Worker (public/sw.js): Seitenaufrufe Network-First mit der zuletzt geladenen index.html als Rückfall, Bundles unter /assets/ (Hash im Namen) aus dem Cache, Icons und Manifest Stale-While-Revalidate. API-Anfragen fasst er nie an. Alte Caches verschwinden beim Aktivieren einer neuen VERSION, Bundles sind auf 80 Einträge begrenzt.
  • Lese-Cache (lib/offline.ts): Jede erfolgreiche GET-Antwort landet pro Benutzer in IndexedDB (mindsift-offline). Ohne Verbindung liefert request() die letzte bekannte Antwort; so startet die App offline angemeldet und zeigt Gedanken, Aufgaben und Listen. Ausgenommen: Streams, Long-Polling, Notiz-Sync (eigener Speicher, modules/collaboration.md), Export, Suche. Abmelden leert Cache und Warteschlange.
  • Warteschlange (Outbox): Offline erfasste Gedanken (POST /inbox) und abgehakte Aufgaben bzw. Listeneinträge (PATCH) werden in localStorage gesammelt, wiederholtes Umschalten zählt nur mit dem Endstand. Gesendet wird in Reihenfolge beim online-Ereignis, beim Start und alle 30 s, solange etwas wartet; vom Server abgelehnte Einträge (4xx) entfallen. Andere Änderungen brauchen weiterhin eine Verbindung.
  • Hinweis: Ein Banner über der unteren Navigation zeigt „Offline“ und die Zahl wartender Änderungen bzw. „Synchronisiere…“.

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.