Files
flowdeck/docs/DATA_MODEL.md
T
bruno 0218d8f5e5
FlowDeck CI / lint (push) Successful in 1m37s
FlowDeck CI / test (push) Failing after 24m19s
FlowDeck CI / docker (push) Skipped
fix: /local-workspace — chemin du header + clic sur un dossier du sidebar (v7.69.9)
Corrige deux régressions de /local-workspace :

- le chemin du header restait bloqué sur « Home / <workspace> » quel que
  soit le dossier affiché : la route rend désormais breadcrumb_items
  (Home / <workspace> / <dossier> / <sous-dossier>, niveaux cliquables,
  collapse « … » au-delà de 4) et la navigation sans rechargement recalcule
  le chemin via l'event flowdeck:breadcrumb-changed ;
- le clic sur un dossier du sidebar affichait TOUS les composants à la
  fois : Alpine.data('wsInitData') retournait le même objet singleton, le
  2e montage (navigation partielle) levait « Cannot redefine property:
  \ » et initTree abandonnait, laissant tout le contenu au state
  brut. La factory retourne désormais une enveloppe fraîche par montage
  qui délègue à l'état réactif partagé. #lw-config est aussi relu à chaque
  exécution (le 2e montage gardait le folder_id du 1er chargement).

Inclus également le travail en cours de l'arbre : Library (colonnes Last
visited/Source, ordre d'en-tête, favoris à icônes Workspace), Meeting
Notes (bloc, CSS, routes, docs), coloration de code hljs, badges
favori/publié dans l'arbre local-workspace, docs (DATA_MODEL,
architectures) et tests associés.
2026-10-09 16:58:04 -04:00

39 KiB
Raw Blame History

FlowDeck — Modèle de données (v7.69.8)

Document de référence du schéma de persistance, généré à partir du code réel :

  • app/db.py — schéma de base (baseline, « version 1 »), créé de façon idempotente par init_db() (lignes 14–839) et connexion SQLite via get_conn() (lignes 843–858) ;
  • app/migrations.py — migrations versionnées (1–38, table schema_version) ;
  • app/services/webhook_outbound.py — table webhook_subscriptions (créée au boot, l. 272–286) ;
  • app/models/requests.py / app/models/responses.py — couche Pydantic de l'API.

Total : 105 tables relationnelles + 1 table virtuelle FTS5 (pages_fts), réparties entre le baseline (app/db.py) et 33 migrations effectuant des créations de tables (app/migrations.py).


1. Moteur de base de données

1.1 SQLite uniquement (pas de dual-engine)

Correction vis-à-vis de certaines notes d'architecture : FlowDeck ne supporte PAS PostgreSQL. Le code est exclusivement SQLite :

  • app/db.py l.4 : import sqlite3 (stdlib) — aucune dépendance psycopg/pg8000/SQLAlchemy nulle part (requirements.txt, pyproject.toml vérifiés) ;
  • le module entier est typé sqlite3.Connection (y compris app/migrations.py l.16) ;
  • fonctionnalités SQLite spécifiques utilisées : PRAGMA journal_mode=WAL, PRAGMA foreign_keys=ON, PRAGMA busy_timeout=5000 (get_conn, l.846–850), INTEGER PRIMARY KEY AUTOINCREMENT, ALTER TABLE … ADD COLUMN (sans IF NOT EXISTS, d'où le pattern try/except sqlite3.OperationalError), déclencheurs (TRIGGER), extension FTS5, DROP TABLE/RENAME pour les reconstructions (comments, tags) ;
  • app/config.py l.58 : database_url: str = "sqlite:////data/flowdeck.db" — seul le préfixe sqlite:/// est interprété par la propriété db_path (l.151–163, avec gestion :memory: et des chemins Windows type C:\…) ; toute autre valeur retombe sur /data/flowdeck.db.

La « compatibilité » mentionnée dans certains documents est donc en réalité : une base SQLite unique par instance, en mode WAL (un writer, multi-readers), avec PRAGMA busy_timeout=5000 pour supporter la concurrence (tests xdist, scheduler d'automations). Les backups (v5.2.0, backup_* dans app/config.py) sont des copies du fichier SQLite.

1.2 Stratégie de migrations

Décrite dans l'en-tête de app/migrations.py (l.1–13) :

Composant Rôle
app/db.py::init_db() Schéma baseline (« version 1 ») : CREATE TABLE IF NOT EXISTS + ALTER garde-fous (try/except OperationalError). Réexécuté à chaque boot, idempotent.
schema_version (version INTEGER PK, name TEXT, applied_at) Registre des migrations appliquées ; version maximale lue par current_version() (l.66–71). Scellée à BASELINE_VERSION = 1 au premier démarrage (l.86–101).
@register(version, name) (l.28–38) Décorateur d'enregistrement dans MIGRATIONS, trié par version, doublons refusés.
apply_migrations(conn) (l.86–111) Appelée en fin de init_db() (app/db.py l.829–831) ; applique exactement une fois chaque migration > version courante.
_apply_one() (l.113–130) Une migration = une transaction (BEGIN explicite, rollback complet à l'échec, puis INSERT INTO schema_version) — DDL tout-ou-rien (audit A31).
columns(conn, table) (l.53–64) Helper unique PRAGMA table_info pour tester l'existence de colonnes avant un ALTER.
fts5_available() (l.74–83) Détection de FTS5 ; sinon repli sur recherche LIKE.

Version de schéma actuelle : 38 (v7.1.1 meeting block, dernier @register). Les migrations 4 et 17 et 23–38 ont été enregistrées hors ordre chronologique de version (versions 4, 17, 23 insérées après 21) — le tri à l'enregistrement garantit l'ordre d'application.


2. Tables par domaine

Légende : PK = clé primaire, FK = clé étrangère (avec cascade quand indiquée). Sauf mention, PK = id INTEGER PK AUTOINCREMENT.

2.1 Kanban & intégration Gitea (héritage v0.x–v1.x)

Créées dans app/db.py (l.22–140, 430–478).

Table But Clés / colonnes clés
boards Board Kanban par dépôt Gitea UNIQUE(project_owner, project_name) ; columns_json (liste des colonnes), wip_limits_json
cards Position d'une issue Gitea dans une colonne FK board_id → boards ; gitea_issue_id, column_name, position, priority, due_date
col_mapping Mapping colonne ↔ label Gitea FK board_id → boards ; UNIQUE(board_id, column_name) ; close_issue
notes Notes par projet UNIQUE(project_owner, project_name, title) ; content
checklists Checklists par issue board_id, gitea_issue_id (pas de FK déclarée), title, position, UNIQUE(board_id, gitea_issue_id, title)
checklist_items Lignes de checklist FK checklist_id → checklists ON DELETE CASCADE ; content, checked, position
project_properties Propriétés par projet (ancêtre de collection_properties, conservé) UNIQUE(project_owner, project_name, name) ; prop_type, options_json
property_values Valeurs de ces propriétés par issue FK property_id → project_properties ON DELETE CASCADE ; UNIQUE(property_id, gitea_issue_id)
ai_keywords Fréquentité des mots-clés détectés par l'IA UNIQUE(project_owner, project_name, keyword) ; color, usage_count
gitea_private_pages Pages privées rattachées à un dépôt Gitea (v2.7) FK user_id → users ; gitea_owner, gitea_repo, content
user_tokens Token Gitea par utilisateur gitea_user_id INTEGER UNIQUE, gitea_token
projects Registre forge-agnostique des projets (gitea/github/builtin), synchronisé par cron (mig. 6, migrations.py l.260–331) UNIQUE(proj_type, owner, name) ; clone_url, default_branch, last_synced_at

2.2 Utilisateurs & authentification

Table But Clés / colonnes clés Emplacement
users Compte local ou fédéré login UNIQUE, email, password_hash, is_admin, is_active, login_attempts, locked_until, auth_method (local|gitea), avatar_url/color, sidebar_config (JSON), notification_prefs (JSON), timezone, totp_secret_enc, totp_backup_hashes (JSON) db.py l.22 ; ALTER db.py l.328–470, mig. 10/11/28
login_history Journal des connexions FK user_id → users ; ip_address, user_agent db.py l.40
user_oauth_tokens Tokens OAuth par provider (Gitea, Google, M365…) FK user_id → users ; UNIQUE(user_id, provider) ; access_token, refresh_token, expires_at db.py l.55
user_sessions Sessions cookie révocables (une ligne = un cookie signé) PK id TEXT (payload du cookie) ; FK user_id → users ON DELETE CASCADE ; revoked, last_seen_at mig. 6 (l.294–316)
api_tokens Tokens Bearer publics (sha256 stocké) FK user_id → users ON DELETE CASCADE ; token_hash UNIQUE, token_prefix, scopes, expires_at, revoked mig. 6 + mig. 20 (l.884–900)
webauthn_credentials Passkeys / 2FA WebAuthn FK user_id → users ON DELETE CASCADE ; credential_id UNIQUE, public_key, sign_count mig. 28 (l.1311–1324)
sso_config Config SSO entreprise (SAML 2.0 ou OIDC), la table prime sur .env dès qu'un admin sauvegarde FK workspace_id → workspaces ; secrets chiffrés au repos (client_secret, sp_private_key — app/services/sso_provisioning.py) ; attribute_mapping/groups_mapping (JSON), active mig. 23 (l.1495–1523)
sso_login_history Audit de chaque tentative SSO (succès + rejets) FK user_id → users ; success, error_message, sso_identifier mig. 23 (l.1525–1545)
sso_requests Store mono-usage anti-replay (AuthnRequest ID, état OIDC, PKCE, relay state) PK id TEXT ; kind, code_verifier, next_path, used mig. 23 (l.1547–1555)
scim_tokens Tokens Bearer de provisioning SCIM (hash unique) token_hash UNIQUE, revoked, FK created_by → users mig. 28 (l.1285–1294)
domain_claims Domaines d'entreprise vérifiés (DNS/txt), auto-jonction + enforce_sso domain UNIQUE, txt_token, FK workspace_id → workspaces mig. 28 (l.1296–1307)
llm_config Config LLM globale de l'instance — ligne unique CHECK (id = 1) provider, model, api_key, api_base, verified, verified_model, last_error ; précédence : ligne DB > settings.llm_* db.py l.769–784
user_llm_keys Clés API LLM par utilisateur et provider FK user_id → users ON DELETE CASCADE ; UNIQUE(user_id, provider) ; models_json (cache des modèles), default_model, verified* db.py l.788–812

2.3 Workspaces, collaboration & permissions

Table But Clés / colonnes clés Emplacement
workspaces Espace de travail multi-utilisateur FK owner_id → users ; settings_json db.py l.222
workspace_members Adhésion + rôle (owner/admin/editor/viewer/commenter) FK workspace_id → workspaces CASCADE, user_id → users ; UNIQUE(workspace_id, user_id) db.py l.230
teamspaces Namespace de pages/bases dans un workspace (v7.3.0) ; private=1 = invisibilité 404 FK workspace_id → workspaces CASCADE ; UNIQUE(workspace_id, name) mig. 29 (l.1380–1391)
teamspace_members Membres + rôle d'un teamspace FK teamspace_id → teamspaces CASCADE, user_id → users ; UNIQUE(teamspace_id, user_id) mig. 29 (l.1393–1403)
user_groups Groupes réutilisables par workspace (ACL) FK workspace_id → workspaces CASCADE ; UNIQUE(workspace_id, name) mig. 18 (l.721–730)
group_members Adhésion à un groupe FK group_id → user_groups CASCADE, user_id → users CASCADE ; UNIQUE(group_id, user_id) mig. 18 (l.732–740)
page_permissions Grant explicite sur une page de l'éditeur FK page_id → pages CASCADE ; user_id XOR group_id (CHECK) ; role (viewer/commenter/editor/owner), grant_type, granted_by mig. 18 (l.745–760)
collection_permissions Idem pour les bases (collections) FK collection_id → collections CASCADE ; mêmes colonnes mig. 18 (l.766–780)
property_permissions Grant par propriété de collection (view/edit) FK collection_id, property_id → collection_properties CASCADE mig. 18 (l.782–797)
permission_audit_log Traînée immuable grant/revoke resource_type, resource_id, action, old_role, new_role, performed_by → users, ip_address mig. 18 (l.799–815)
page_shares Partage de page (user, groupe ou email) FK page_id → pages CASCADE, shared_with_user_id → users, shared_with_group_id → user_groups ; permission (view/edit/comment) db.py l.451–478
guest_shares Accès sans compte via /g/<token> (v7.3.0) FK page_id → pages CASCADE ; token UNIQUE, role, expires_at, revoked mig. 29 (l.1440–1452)
favorites Favoris d'un utilisateur (reconstruite en v2.2 pour pointer pages) FK user_id → users, page_id → pages ; UNIQUE(user_id, page_id) db.py l.260–270
recents Pages récemment accédées (vue « Recents ») FK user_id → users, page_id → pages ; source_type (local/gitea…), UNIQUE(user_id, page_id) db.py l.481–493
notifications Notifications in-app (mention/commentaire/page) FK user_id → users CASCADE, actor_id → users ; ntype, resource_type/resource_id, is_read ; index (user_id, is_read) db.py l.592–613
comments Commentaires (fil, résolus) ancrés sur pages ou lignes de base — reconstruite v4.9 : cible polymorphe FK user_id → users ; target_type (page | collection_page) + target_id, parent_id (réponses), anchor_block_id/anchor_start/anchor_end (ancre inline) db.py l.241 (v2.0) puis l.620–649 (v4.9)
comment_reactions Réactions emoji d'un commentaire FK comment_id → comments CASCADE, user_id → users CASCADE ; UNIQUE(comment_id, user_id, emoji) mig. 29 (l.1418–1428)
text_reactions Réactions ancrées sur une plage de texte d'un bloc (façon Notion, v7.64) FK page_id → pages CASCADE, user_id → users CASCADE ; block_id TEXT, offsets anchor_start/anchor_end, UNIQUE(page_id, block_id, start, end, emoji, user_id) mig. 37 (l.1683–1707)
page_follows Abonnement aux changements d'une page PK composite (page_id → pages CASCADE, user_id → users CASCADE) mig. 29 (l.1430–1437)
page_verifications Badge « page vérifiée ✅ » avec expiration PK page_id → pages CASCADE (1 par page) ; verified_by → users, expires_at mig. 29 (l.1407–1416)
tags Étiquettes par utilisateur (user_id=0 = globales) FK user_id → users ; UNIQUE(name, user_id) ; color db.py l.282–290
page_tags Association page ↔ tag PK composite (page_id → pages CASCADE, tag_id → tags CASCADE) db.py l.292–299
custom_emojis Emojis personnalisés uploadés par workspace FK workspace_id (défaut 1, non déclaré) ; name, url mig. 8 (l.371–388)

2.4 Pages & éditeur de blocs

Table But Clés / colonnes clés Emplacement
pages Page de l'éditeur (wiki) — table pivot du produit FK workspace_id → workspaces, parent_id → pages (hiérarchie), collection_id → collections (base plein écran, v4.6), collection_row_id → collection_pages CASCADE (page-ombre de contenu d'une ligne de base, v6.5, mig. 22 l.997–1020), locked_by → users ; colonnes notables : content (markdown legacy ou JSON de blocs selon content_format), content_format, parent_section, share_mode, published/is_published/publish_slug/is_shared, deleted_at (corbeille), sort_order, cover_url, page_icon, is_locked, full_width, font_small, search_excluded, permission_type (inherit/restricted/private), teamspace_id (ALTER, sans FK déclarée), sync_version (trigger de concurrence optimiste, mig. 17) db.py l.154–166 + ALTER l.328–470 ; mig. 7/13/18/22/24/25/29
page_versions Snapshots annulables de l'éditeur de blocs (undo/restore, une ligne par save) FK page_id → pages CASCADE ; title, blocks_json (liste complète des blocs), note, user_id → users mig. 7 (l.336–365)
page_history Historique legacy des lignes de base (collection_pages) FK page_id → collection_pages CASCADE ; change_type, snapshot_json db.py l.249
page_global_templates Modèles de page globaux créés par les utilisateurs blocks_json (format éditeur), FK created_by → users SET NULL mig. 13 (l.582–597)
synced_blocks Bloc synchronisé : source de vérité du contenu, réutilisé sur plusieurs pages content (JSON blocs), created_by → users (sans FK), workspace (TEXT), synced via page_synced_blocks mig. 15 (l.626–662)
page_synced_blocks Référence page ↔ bloc synchronisé (désynchronisable par page) FK page_id → pages CASCADE, synced_block_id → synced_blocks CASCADE ; block_index, UNIQUE(page_id, synced_block_id) mig. 15 (l.646–662)
page_views Compteur de vues quotidien par page (même pattern que site_views) FK page_id → pages CASCADE ; PK composite (page_id, day), views mig. 29 (l.1454–1461)
pages_fts Table virtuelle FTS5 (title, body) pour la recherche plein texte de la palette de commandes ; synchronisée par 3 triggers AFTER INSERT/UPDATE/DELETE sur pages ; backfill au boot ; repli LIKE si FTS5 absent rowid = pages.id mig. 3 (l.156–205)

2.5 Bases de données (collections, style Notion)

Table But Clés / colonnes clés Emplacement
collections Base de données (collection) schema_json (schéma des propriétés), FK gitea_owner/gitea_repo (texte, verrouillage sync), workspace_id → workspaces, created_by → users, is_locked, is_inline + parent_page_id (base inline), is_task + task_assignee_prop/task_status_prop/task_due_prop → collection_properties SET NULL (mapping My Tasks, mig. 30 l.1558–1585), permission_type, teamspace_id (sans FK), form_config_json, sync_version db.py l.172 + ALTER db.py l.495–520, mig. 18/24/29/30
collection_pages Ligne d'une base (row) FK collection_id → collections CASCADE, parent_id → collection_pages (hiérarchie interne), property_values_json (valeurs par propriété), gitea_issue_id/gitea_issue_number (sync Kanban), cover_url, external_event_id (calendrier, mig. 27), permission_type, sync_version db.py l.187 ; mig. 10/18/27
collection_views Vues sauvegardées par base (table/board/calendar/gallery/list…) FK collection_id → collections CASCADE ; view_type, config_json (filtres/tri/groupement/columns visibles), position, created_by (NULL = vue partagée, sinon personnelle — mig. 10) db.py l.210 ; mig. 10 (l.472–478)
collection_properties Schéma des propriétés d'une base (remplace project_properties) FK collection_id → collections CASCADE ; prop_type, options_json (select/multi-select), number_format, relationnels : related_collection_id → collections SET NULL, relation_property_id/target_property_id → collection_properties SET NULL, rollup_function, formula_expression, validation_json (mig. 4), group_name (mig. 10), button_automation_id → automations SET NULL (bouton natif, mig. 26) ; UNIQUE(collection_id, name) db.py l.222–241 ; mig. 4/10/26
collection_data_sources Sources de données liées (base liée type Notion « Linked database ») FK collection_id → collections CASCADE, source_collection_id → collections ; is_linked, UNIQUE(collection_id, source_collection_id) db.py l.480–493
collection_dashboards Dashboard = combinaison de vues/widgets sur une page FK collection_id → collections CASCADE ; layout_json ({"columns":1,"widgets":[]}) db.py l.531–542
database_templates Modèles de bases prêts à l'emploi (seed idempotent, v5.3.0 : Meeting notes, etc.) name, icon, description, schema_json db.py l.272 + mig. 4 (l.390–411) ; seed via app/services/db_templates.py::SEED_TEMPLATES
page_templates Modèles de ligne par base, éventuellement récurrents FK collection_id → collections CASCADE ; property_values_json, content_json (blocs), is_recurring + recurrence_rule db.py l.294–303 + ALTER l.512–522
page_dependencies Dépendances entre lignes (bloque / bloqué par) FK page_id, dependency_id → collection_pages CASCADE ; dependency_type, auto_shift (overlap/…) ; UNIQUE(page_id, dependency_id, dependency_type) db.py l.551–563
sprints Sprints Scrum par base de tâches FK collection_id → collections CASCADE ; start_date/end_date (TEXT), status (planning/active/done), goal, auto_complete db.py l.570–582
sprint_pages Lignes affectées à un sprint + vélocité FK sprint_id → sprints CASCADE, page_id → collection_pages CASCADE ; velocity_points, status_at_start, UNIQUE(sprint_id, page_id) db.py l.584–594
reminder_log Journal anti-doublon des rappels : 1 ligne par (ligne de base, date d'occurrence) — le rappel ne fire qu'une fois même après restart FK page_id → collection_pages CASCADE ; UNIQUE(page_id, occurrence_date) ; alimenté par le scan reminders_enabled (app/config.py l.75–77) — il n'existe pas de table reminders ; les dates de rappel proviennent des propriétés due des lignes mig. 11 (l.485–509)

2.6 Automatisation, workers & webhooks

Table But Clés / colonnes clés Emplacement
automations Règles if-this-then-that (moteur v5.1.0) : trigger_type event/cron/button, event, cron_expression, collection_id (TEXT sans FK, scope optionnel), condition_json (clauses), actions_json (descripteurs), enabled, run_count, last_run_at, trigger_mode (any|all, fenêtre de 5 min, mig. 26) mig. 5 (l.208–257) + mig. 26 (l.1196–1201)
automation_steps Étapes ordonnées d'une automatisation v2 (trigger/condition/delay/action, v7.0.0) ; l'engine retombe sur condition_json/actions_json si aucune step FK automation_id → automations CASCADE ; kind, position, config_json mig. 26 (l.1156–1172)
automation_runs Historique d'exécution (audit + UI Settings) FK automation_id → automations CASCADE ; status (fired/skipped/error), trigger_source, collection_id/page_id (TEXT/INT sans FK déclarée), detail mig. 5 (l.242–257)
workers Snippets Python sandboxés (cron ou manuel), partageables (v7.0.0) slug UNIQUE, FK workspace_id → workspaces CASCADE, code_py, schedule_cron, shared, daily_budget_s (budget CPU quotidien), created_by → users (sans FK déclarée) mig. 26 (l.1174–1192)
worker_runs Logs d'exécution des workers FK worker_id → workers CASCADE ; status, logs, duration_ms mig. 26 (l.1194–1210)
webhook_subscriptions Abonnement sortant URL + event + secret (créée au boot hors migrations, appelée par init_db l.834–837) url, event, secret, active app/services/webhook_outbound.py l.272–286
webhook_deliveries Journal de livraison (retry ledger) FK webhook_id → webhook_subscriptions CASCADE ; status (pending/retrying/success/error/superseded), http_code, attempt, event, next_retry_at (epoch, scheduler v6.4), payload (JSON), duration_ms mig. 20 (l.913–928) + mig. 21 (l.940–957)
idempotency_keys Support d'Idempotency-Key sur les POST de l'API v2 (réponse rejouée) PK key TEXT ; FK user_id → users CASCADE ; response_json, status_code mig. 20 (l.930–938)

2.7 IA / Agent FlowDeck

Table But Clés / colonnes clés Emplacement
agents Définition d'agent (personnel ou workspace) FK workspace_id → workspaces, created_by → users ; agent_type, system_instructions, model, scope_json, trigger_json, approval_mode (auto/gated), is_active, UNIQUE(workspace_id, name) db.py l.660–680
agent_conversations Conversation d'un utilisateur avec un agent FK agent_id → agents CASCADE, user_id → users ; status, context_json, override provider/model (ALTER db.py l.820–827), memory_enabled (mig. 31) db.py l.681–691
agent_messages Messages (rôles user/assistant/tool), coût tokens FK conversation_id → agent_conversations CASCADE ; tool_calls_json, model, tokens_used db.py l.692–702
agent_actions Journal des tool calls exécutés (audit + undo) FK conversation_id → … CASCADE ; tool_name, target_type/target_id, payload_json, result_json, status, undo_snapshot_json, executed_by → users db.py l.703–716
agent_skills Skills réutilisables (prompt + outils autorisés) FK workspace_id → workspaces, created_by → users ; prompt_template, allowed_tools_json, UNIQUE(workspace_id, name) db.py l.717–729
agent_triggers Déclencheurs d'agent (manual/cron/événement) FK agent_id → agents CASCADE ; trigger_type, config_json, is_active, last_fired_at db.py l.730–740
agent_feedback 👍/👎 sur les réponses (rating CHECK up/down) FK conversation_id/message_id/user_id en SET NULL ; snippet, comment db.py l.742–756
agent_policies Gouvernance par workspace (1 ligne/workspace) FK workspace_id → workspaces CASCADE, UNIQUE(workspace_id) ; allowed_tools_json, max_steps, require_approval mig. 28 (l.1326–1338)
agent_approvals File d'approbation des actions d'écriture gated conversation_id (INT sans FK déclarée), tool, args_json, status (pending/…), requester_id/approver_id → users mig. 28 (l.1340–1354)
agent_memory Résumé mémorisé par conversation (upsert 1:1, v7.54.0) PK conversation_id → agent_conversations CASCADE ; kind (summary), content mig. 31 (l.1661–1681)
agent_connectors Catalogue de connecteurs d'agent (custom/Discord/Telegram/MCP, v7.55–7.57) name, url, secret_encrypted (Fernet), kind, auth (bearer/…), tools_json (cache d'outils MCP), enabled, status, created_by (sans FK) mig. 32 (l.1640–1659) + mig. 34 (l.1613–1624)
connector_tokens Tokens OAuth par (kind, user) — Google / M365, chiffrés Fernet PK composite (kind, user_id) ; tokens_enc mig. 33 (l.1626–1638)
plugins Registre des modules activables/désactivables de l'instance (web-tools, web-clipper, automations) — seed idempotent PK slug TEXT ; enabled mig. 35 (l.1589–1611)
semantic_embeddings Recherche sémantique hybride + Ask AI (v6.9) : vecteurs « hashed-TF » 256 dims sans dépendance externe ; chunks par ressource polymorphe PK composite (resource_type, resource_id, chunk_id) (page | collection…) ; chunk_text, embedding BLOB, model mig. 25 (l.1096–1133)
semantic_index_state État d'indexation incrémentale (dernier index par ressource) PK composite (resource_type, resource_id) ; indexed_at mig. 25 (l.1122–1133)

2.8 Sites publics & formulaires

(mig. 24, migrations.py l.1021–1093)

Table But Clés / colonnes clés
sites Mini-site public multi-pages (façon Notion Sites) slug UNIQUE, FK root_page_id → pages CASCADE, custom_domain UNIQUE, theme, password_hash, expires_at, noindex, analytics_id, created_by → users
site_pages Arbre public ordonné des pages du site FK site_id → sites CASCADE, page_id → pages CASCADE ; position, UNIQUE(site_id, page_id)
site_views Compteur de vues jour/site (upsert, pas d'IP brute — RGPD) FK site_id → sites CASCADE ; PK composite (site_id, day), views
form_responses Journal anonymisé des soumissions de formulaire public FK collection_id → collections CASCADE, row_id → collection_pages SET NULL (ligne créée) ; ip_hash (hash tournant quotidien, pas d'IP) ; config du form dans collections.form_config_json

2.9 Calendrier & réunions

Table But Clés / colonnes clés Emplacement
calendar_links Lien par utilisateur entre une collection et un calendrier externe (Google REST ou CalDAV générique) FK user_id → users CASCADE, collection_id → collections CASCADE ; provider, tokens_enc (Fernet), calendar_id, date_property, sync_token, UNIQUE(user_id, provider, calendar_id) mig. 27 (l.1231–1245)
meeting_transcripts Note de réunion (audio + transcription + résumé IA), machine à états du bloc Notion-like (idle/recording/paused/processing/done/failed) FK page_id → pages CASCADE ; audio_path, transcript, summary, language ; v7.1.1 : status, mode, channels_json, instruction_snapshot, consent_json, segments_json, processing_steps_json, summary_json, generated_title, completeness_class, quality_score, duration_ms/paused_ms, event_occurrence_id (index unique partiel : une occurrence calendrier = au plus une note) mig. 27 (l.1247–1271) + mig. 38 (l.1730–1800)
meeting_consent_log Journal append-only du consentement d'enregistrement FK transcript_id → meeting_transcripts CASCADE ; method (start_attestation/…), attested_by → users, participants_json mig. 38 (l.1767–1777)
meeting_distributions Journal append-only des diffusions de compte-rendu (copy_link/…) FK transcript_id → meeting_transcripts CASCADE ; channel, actor_id → users, recipients_json, status mig. 38 (l.1779–1788)

2.10 Import/export & offline (PWA, clipper)

Table But Clés / colonnes clés Emplacement
import_jobs Job d'import asynchrone (polling UI) — Notion/CSV/markdown PK id TEXT (uuid) ; source, filename, status (queued/running/done/error), error, report_json mig. 9 (l.434–451)
import_items Dédup : 1 ligne par page importée, clé (workspace, source, external_id) → re-import idempotent FK workspace_id/page_id sans REFERENCES déclaré ; UNIQUE(workspace_id, source, external_id) mig. 9 (l.418–432)
offline_sync_queue File serveur des mutations reçues des clients hors-ligne (/api/v2/sync/batch), rejouables/auditées par device FK user_id → users CASCADE ; device_id, type (page_create/update/delete/move, collection_*), payload (JSON), client_timestamp, server_version, status (pending/syncing/synced/failed), retries, error ; détection de conflit édit/édit via sync_version (mig. 17, triggers sur pages, collection_pages, collections) mig. 16 (l.668–700)
extension_devices Devices de l'extension Web Clipper (token hashé, scopes) FK user_id → users CASCADE ; device_id, token_hash, scopes, revoked, UNIQUE(user_id, extension_name, device_id) mig. 19 (l.838–857)
extension_clips Clippings capturés (page cible + workspace cible) FK user_id → users CASCADE, target_page_id → pages SET NULL, target_workspace_id → workspaces SET NULL ; clip_type, source_url mig. 19 (l.861–877)

2.11 API & audit

Table But Clés / colonnes clés Emplacement
api_audit_log Traînée immuable des mutations de l'API publique v2 FK user_id → users SET NULL, token_id → api_tokens SET NULL ; action, resource_type, resource_id, ip_address, detail mig. 20 (l.895–911)
schema_version Registre des migrations appliquées (meta) PK version INTEGER ; name, applied_at app/migrations.py l.41–51

2.12 Divers

favorites, recents, notes, ai_keywords, login_history déjà couverts ci-dessus. À noter également : les tables temporaires de reconstruction vues dans le code (comments_new, tags_new) ne sont pas des tables permanentes (pattern recreate → copy → drop → rename).


3. Relations clés (graphe FK simplifié)

graph TD
    USERS[users]
    WS[workspaces]
    PAGES[pages]
    COLS[collections]
    CP[collection_pages]
    CPV[collection_properties]
    CV[collection_views]

    WS -->|owner_id| USERS
    WS -->|workspace_members| USERS
    WS -->|workspace_id| PAGES
    WS -->|workspace_id| COLS
    PAGES -->|parent_id self| PAGES
    PAGES -->|collection_id| COLS
    PAGES -->|collection_row_id CASCADE| CP
    CP -->|collection_id CASCADE| COLS
    CP -->|parent_id self| CP
    CPV -->|collection_id CASCADE| COLS
    CPV -->|related_collection_id| COLS
    CPV -->|relation/target_property_id self| CPV
    CPV -->|button_automation_id| AUTO[automations]
    CV -->|collection_id CASCADE| COLS
    AUTO -->|collection_id texte sans FK| COLS
    AGENTS[agents] -->|workspace_id| WS
    CONV[agent_conversations] -->|agent_id CASCADE| AGENTS
    CONV -->|user_id| USERS
    MSG[agent_messages + actions] -->|conversation_id CASCADE| CONV
    GRP[user_groups] -->|workspace_id CASCADE| WS
    PPERM[page/collection/property_permissions] -->|user_id XOR group_id| USERS
    PPERM -->|group_id| GRP
    SITES[sites/site_pages] -->|root/page CASCADE| PAGES
    TRANS[meeting_transcripts] -->|page_id CASCADE| PAGES
    CAL[calendar_links] -->|user_id| USERS
    CAL -->|collection_id CASCADE| COLS
    CLIPS[extension_clips] -->|target_page_id| PAGES

Points de relation à retenir :

  1. users est le hub d'identité : plus de 30 FK entrantes (sessions, tokens, permissions, audit, contenu). workspaces est le hub d'isolation multi-tenant (owner_id, workspace_members définit le rôle).
  2. Hiérarchie pages ↔ collection_pages : deux univers de « pages ». pages = éditeur de blocs wiki (FK self parent_id, soft-delete deleted_at, corbeille) ; collection_pages = lignes de base. Le pont v6.5 : chaque ligne peut avoir une page-ombre pages.collection_row_id (CASCADE) qui porte son contenu blocs — versions, synced blocks et temps réel opèrent sur elle.
  3. Chaîne de cascades principales : collections → collection_pages/collection_views/collection_properties → … et pages → page_versions/page_shares/page_synced_blocks/page_permissions/guest_shares/text_reactions. Les suppressions de ressource nettoyent l'éditorial ; les grants d'audit (*_audit_log, api_audit_log, permission_audit_log) privilégient SET NULL sur l'acteur pour survivre à la suppression du compte.
  4. Permissions en 3 niveaux (mig. 18) : pages/collections/collection_pages.permission_type (inherit|restricted|private) pilote si les tables *_permissions (user XOR group via CHECK) font foi ; les groupes (user_groups/group_members) sont workspace-scopés ; page_shares reste le chemin de partage simple, guest_shares le chemin sans compte.
  5. Sync sémantique Gitea : boards/cards/col_mapping/checklists/property_values et collection_pages.gitea_issue_id + collections.gitea_owner/repo relient les entités FlowDeck aux issues Gitea (jointures par ID numérique, pas de FK — système externe).
  6. Polymorphisme non-FK : notifications.resource_type/resource_id, semantic_embeddings.resource_type/resource_id, permission_audit_log.resource_type/resource_id, import_items.page_id, agent_approvals.conversation_id, automations.collection_id, custom_emojis.workspace_id, teamspaces-id sur pages/collections (ALTER sans FK) — cohérence maintenue applicativement, pas par le moteur.

4. Colonnes JSON structurantes

Le modèle est fortement « document-oriented » : le JSON TEXT est le format de stockage des structures riches (choix assumé du SQLite sans JSON natif, requêtage via json_extract côté applicatif).

Domaine Table.colonne Contenu
Kanban boards.columns_json / wip_limits_json liste ordonnée des colonnes ; limites WIP par colonne
Kanban/legacy project_properties.options_json options de select
Collections collections.schema_json schéma complet des propriétés d'une base (déclaratif, dupliqué avec collection_properties)
Collections collections.form_config_json formulaire public (champs, validations)
Lignes collection_pages.property_values_json map {property_name → valeur} de la ligne
Vues collection_views.config_json config de vue : filtres, sorts, groupements, colonnes visibles/cache, sizing — cœur du système de vues multiples
Propriétés collection_properties.options_json / validation_json options select, règles de validation
Dashboard collection_dashboards.layout_json colonnes + widgets
Modèles database_templates.schema_json, page_templates.property_values_json / content_json blueprint de base / ligne
Éditeur pages.content (avec content_format='blocks') tableau JSON des blocs (heading, todo, callout, synced_block, meeting, etc.) — l'unité de contenu fondamentale
Historique page_versions.blocks_json, page_history.snapshot_json, page_global_templates.blocks_json, synced_blocks.content snapshots de liste de blocs
Workspace workspaces.settings_json, users.sidebar_config, users.notification_prefs, users.totp_backup_hashes préférences
Agent agents.scope_json/trigger_json, agent_conversations.context_json, agent_messages.tool_calls_json, agent_actions.payload_json/result_json/undo_snapshot_json, agent_skills.allowed_tools_json, agent_triggers.config_json, agent_policies.allowed_tools_json, agent_approvals.args_json orchestration, audit et undo
LLM user_llm_keys.models_json cache des modèles distants
Automatisation automations.condition_json/actions_json, automation_steps.config_json DSL des règles (fallback steps ↔ legacy colonnes)
Sync offline offline_sync_queue.payload, idempotency_keys.response_json mutation client + réponse rejouée
Webhooks webhook_deliveries.payload corps livré
Réunions meeting_transcripts.channels_json/consent_json/segments_json/processing_steps_json/summary_json, meeting_consent_log.participants_json, meeting_distributions.recipients_json pipeline transcription
Imports import_jobs.report_json rapport par fichier
SSO sso_config.attribute_mapping, groups_mapping, sso_login_history (mapping) attributs d'assertion

5. Couche Pydantic (app/models/)

Les modèles Pydantic ne couvrent qu'une petite partie du contrat API — la majorité des routes FastAPI valide en dur dans les handlers :

  • app/models/requests.py (75 l.) :
    • FileSaveRequest — sauvegarde de fichier via Gitea (path, content, message, sha optionnel pour update) ; @model_validator(mode="after") restreint l'extension via ALLOWED_EXTENSIONS/_ext (app/middleware/security) ;
    • IssueCreateRequest / IssueUpdateRequest — création/mutation d'issues Gitea (title ≤ 500, state pattern ^(open|closed)$, labels/milestone/assignee en ids virgule) ;
    • CardMoveRequest — déplacement de carte Kanban (owner/repo/issue_id/column) ;
    • ColMappingRequest — mapping colonne ↔ label ;
    • UploadValidationResult — DTO de validation d'upload (filename/size/extension/valid/error).
  • app/models/responses.py (38 l.) : deux enveloppes standardisées, utilisées en response_model OpenAPI des routes publiques :
    • ErrorResponse {error, detail?} ;
    • SuccessResponse {status="ok", data?: dict}.

Aucun modèle ORM (pas de SQLAlchemy) : l'accès aux données est du SQL brut sqlite3 via le context-manager get_conn() (app/db.py l.843–858, row_factory = sqlite3.Row), et les conversions dict↔JSON des colonnes *_json sont faites manuellement dans les services.


6. Fiches de synthèse technique

Aspect Valeur réelle (vérifiée dans le code)
Moteur SQLite unique, fichier settings.db_path (database_url = sqlite:///…), WAL + busy_timeout=5000, foreign_keys=ON
ORM aucun — SQL brut
Pilote sqlite3 stdlib
Migrations maison, versionnées : baseline v1 (init_db idempotent) + registry @register (app/migrations.py), table schema_version, transaction par migration
Version de schéma 38
Triggers FTS5 (pages_fts_ai/ad/au), bumps sync_version (*_sync_version_bu) sur pages/collection_pages/collections
Full-text FTS5 optionnel, repli LIKE
Recherche vectorielle semantic_embeddings (hash TF 256-dim en BLOB, PK composite polymorphe, pas d'extension vec0)
Chiffrement au repos Fernet sur calendar_links.tokens_enc, connector_tokens.tokens_enc, secrets SSO ; hash sha256 pour api_tokens.token_hash, scim_tokens.token_hash, extension_devices.token_hash ; password_hash (users)
Sauvegardes copie planifiée du fichier SQLite (backup_*, app/config.py l.66–70)
Concurrence verrou applicatif d'écriture indirect via WAL + busy_timeout ; migration async du 510 call sites synchrones encore en cours (note A21, app/db.py l.848–852)