"Passwort vergessen?" appears on the login screen when SMTP and BASE_URL are configured. The request always gets the same answer and sends the mail in the background (at most every 2 minutes per account). The link holds a random token valid once for one hour; only its SHA-256 is stored. Setting the new password invalidates all open links and ends every session. The app removes the token from the address bar and offers a way back for expired links. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8.9 KiB
Datenbank
SQLite-Datei mindsift.db (Standard: $DATA_DIR/mindsift.db), geöffnet mit journal_mode=WAL, synchronous=NORMAL, foreign_keys=ON, busy_timeout=10000 und höchstens 8 Verbindungen. Das Schema entsteht in Store.migrate() (sqlite.go): zuerst CREATE TABLE IF NOT EXISTS, danach eine Liste idempotenter ALTER TABLE/CREATE-Anweisungen für ältere Datenbanken. IDs sind Text mit Präfix (usr_…, th_…, tkl_…, lst_…, tls_…, blk_…, doc_…).
Die Datei migrations/001_init.sql wird vom Code nicht gelesen und ist veraltet (FLAW-13).
Tabellen
Benutzer und Zugang
| Tabelle | Zweck | Wichtige Spalten |
|---|---|---|
users |
Konten | email (eindeutig), username, password_hash, display_name, role (admin/member), is_active, token_version, alarm_channels (kommagetrennt), alarm_email, discord_webhook_url, custom_webhook_url, notify_before_minutes, avatar_url, organization_id (leer = keine Organisation) |
user_invites |
Einladungscodes | code, email, role, organization_id, expires_at, used_by |
revoked_tokens |
Sperrliste ausgeloggter JWTs | token_hash, expires_at |
user_themes |
Eigene CSS-Themes eines Benutzers | user_id (FK, ON DELETE CASCADE), name, css_content |
api_keys |
Webhook-/API-Schlüssel | key_prefix, key_hash (SHA-256), scopes (nicht ausgewertet), last_used_at |
system_settings |
Schlüssel/Wert für Instanz-Einstellungen | sharing_mode, allow_registration, instance_name, smtp_* |
Gedanken und Aufgaben
| Tabelle | Zweck | Wichtige Spalten |
|---|---|---|
password_reset_tokens |
Links aus „Passwort vergessen?“ | token_hash (SHA-256), user_id, created_at, expires_at (1 h), used_at |
thoughts |
Gedanken und Aufgaben in einer Tabelle | status (inbox/kept/converted/discarded), converted_type (task/document/list_item/document_block), converted_ref_id, priority, is_completed, due_date (YYYY-MM-DD ganztägig oder YYYY-MM-DDTHH:MM, Zeitzone des Servers), task_list_id, kanban_status, notes, subtasks (JSON), tags (JSON), alarm_sent, sort_order, source (manual, voice, webhook …) |
task_assignees |
Zuweisungen von Aufgaben | task_id (FK thoughts, ON DELETE CASCADE), user_id, created_at; nur Personen mit Zugriff auf die Liste, bereinigt durch PruneTaskAssignees |
task_lists |
Aufgabenlisten | user_id (Eigentümer), title, icon, color, columns (JSON, Kanban-Spalten) |
task_list_shares |
Freigaben von Aufgabenlisten | target_type (team/user), target_id, permission (editor/viewer, Standard editor) |
Eine Aufgabe ist ein thought mit status='kept' oder status='converted' AND converted_type='task'. Aufgaben, die aus einem Notiz-Block entstehen, tragen converted_type='document_block' und converted_ref_id='<docId>:<blockId>'.
Workspace
| Tabelle | Zweck |
|---|---|
documents |
Notizen: title, icon, folder_path, tags (JSON), is_archived (1 = im Papierkorb), archived_at, archived_by (wer gelöscht hat), markdown_cache, shared_folder_id, sub_folder_path, head_seq (letzte Log-Position), title_version, log_floor (bis hierhin ist das Log verdichtet), Altspalten content, blocks_json |
blocks |
Outliner-Blöcke als Baum: document_id, parent_id ('' = oberste Ebene), order_key (Sortierschlüssel unter Geschwistern), type, content, checked, collapsed, lang, version (Textversion), conflict_of/conflict_author (Konfliktkopien), updated_by, deleted (Grabstein) und deleted_at (Zeitpunkt des Löschens, für das Aufräumen). level/position sind Altspalten und werden nicht mehr gepflegt; Ebene und Reihenfolge berechnet der Server beim Lesen aus dem Baum. |
doc_ops |
Änderungs-Log je Notiz: seq (fortlaufend pro Notiz), op_id, user_id, client_id, kind, block_id, payload (Effekt als JSON), created_at. Abspielen aller Einträge ab einem Snapshot ergibt den Stand. Wird verdichtet (siehe unten). |
doc_op_results |
Ergebnis je Client-Operation (op_id eindeutig) für idempotente Wiederholungen; wird nach 90 Tagen gelöscht |
block_tags |
#Hashtags je Block (tag, document_id, block_id) |
tag_pages |
Freitext-Notiz je Benutzer und Tag |
doc_pins |
Favoriten je Benutzer |
doc_shares, doc_team_shares |
Notiz-Freigaben an Benutzer bzw. Gruppen (permission: read = Leser, write = Bearbeiter) |
workspace_folders |
Explizit angelegte Ordnerpfade |
workspace_shared_folders |
Geteilte Ordner (Entität mit Eigentümer) |
workspace_folder_shares |
Freigaben geteilter Ordner |
workspace_folder_mounts |
Persönlicher Einhängepfad je Empfänger und geteiltem Ordner |
doc_user_folders |
Persönliche Ablage einer geteilten Einzelnotiz je Empfänger |
Blocktypen: h1, h2, h3, paragraph, text, todo, bullet, code, callout, divider, table, progress.
Listen
| Tabelle | Zweck |
|---|---|
lists |
Geteilte Listen: title, icon, list_type (shopping/checklist/vacation) |
list_shares |
Freigaben: target_type, target_id, role (editor/viewer) |
list_items |
Einträge: content, category, quantity, is_completed, completed_by(_name), created_by (leer bei alten Einträgen), sort_order |
Gruppen
| Tabelle | Zweck |
|---|---|
teams |
Gruppen der Benutzer: name, description, org_id (Organisation des Erstellers) |
team_members |
team_id, user_id, role (admin/member) |
organizations |
Vom Server-Admin angelegt: name. Mitglieder über users.organization_id (höchstens eine pro Benutzer) |
Beziehungen
users 1─n thoughts thoughts n─1 task_lists (task_list_id, lose)
users 1─n task_lists task_lists 1─n task_list_shares ─(team|user)→ teams/users
users 1─n documents 1─n blocks 1─n block_tags
documents n─n users via doc_shares / doc_team_shares (nur Lesen)
workspace_shared_folders 1─n workspace_folder_shares / workspace_folder_mounts
users 1─n lists 1─n list_items; lists 1─n list_shares
teams 1─n team_members n─1 users
thoughts.task_list_id ist keine Fremdschlüsselbeziehung; beim Löschen einer Liste setzt der Code die Verweise auf ''.
Migrationen und Aufräumarbeiten beim Start
- Spalten/Tabellen/Indizes werden nachgezogen (
ALTER TABLE … ADD COLUMN, Fehler werden ignoriert). - Veraltete Tabellen werden gelöscht:
spaces,space_members,space_team_shares,document_crdt_state,document_crdt_updates,document_blocks,doc_block_tags. users.usernamewird aus dem E-Mail-Präfix aufgefüllt.- Notiz-Synchronisation (
migrateDocSync): neue Spalten anblocks(inkl.deleted_at; vorhandene Grabsteine erhalten den Migrationszeitpunkt) unddocuments(head_seq,title_version,log_floor), Tabellendoc_opsunddoc_op_results. Blöcke im alten flachen Format (nurlevel/position) werden einmalig in den Baum umgewandelt. - Standardwerte in
system_settings(INSERT OR IGNORE):sharing_mode=organizations,allow_registration=false,instance_name=MindSift, SMTP leer. - Schema-Version:
PRAGMA user_versionhältschemaVersion(derzeit 2), damit spätere Versionen erkennen, welche Migrationen eine Datenbank schon hat. Daten werden beim Start nie gelöscht (das einmalige Zurücksetzen auf Version 2 ist entfernt, alle Installationen sind darüber hinweg). Nur der Demo-Modus leert die Datenbank bei jedem Start.
Sicherung
Die gesamte Instanz besteht aus der Datei mindsift.db (plus -wal/-shm im laufenden Betrieb). Konsistent sichern per sqlite3 mindsift.db ".backup 'backup.db'" oder durch Stoppen des Containers und Kopieren des data/-Verzeichnisses. Benutzerexport: GET /api/v1/export (JSON mit Gedanken, Notizen, Listen, Aufgabenlisten).
Verdichtung des Notiz-Logs
Store.CompactDocs läuft beim Start und danach alle 6 Stunden (startCompaction in internal/api/docsync.go):
| Was | Aufbewahrung | Folge |
|---|---|---|
doc_ops geteilter Notizen |
30 Tage | ältere Einträge werden gelöscht, documents.log_floor merkt sich die Grenze |
doc_ops privater Notizen |
7 Tage | wie oben |
Grabsteine in blocks |
30 Tage nach deleted_at |
endgültig gelöscht; das ist selbst ein Log-Eintrag purge, geöffnete Editoren übernehmen ihn ohne Neuladen. Ein Grabstein bleibt, solange ein jüngerer gelöschter Unterblock auf ihn zeigt. |
doc_op_results |
90 Tage | gelöscht |
Ein Client, dessen lastSeq unter log_floor liegt (z. B. Gerät wochenlang offline), bekommt von /sync resync: true und lädt den Snapshot neu; seine noch nicht gesendeten Änderungen bleiben in der Warteschlange und werden danach angewendet. Text für einen inzwischen endgültig gelöschten Block wird als neuer Block am Ende der Notiz angelegt, ein neuer Block unter einem endgültig gelöschten Elternblock landet auf der obersten Ebene.