Base de données — Schéma SQLite

Informations générales

Diagramme ER (Phase 65)

erDiagram
    users ||--o{ projects : owns
    users ||--|| account_settings : has
    users ||--o{ recovery_keys : holds
    users ||--o{ subscriptions : pays
    users ||--o{ invites : creates
    users ||--o{ chat_messages : sends
    users ||--o{ trial_credits : claims
    users ||--o{ activity_log : generates

    projects ||--o{ project_phases : tracks
    projects ||--o{ pinned_notes : pins
    projects ||--o{ chat_messages : contains
    projects ||--o{ timeline_events : records
    projects ||--o{ github_links : links
    projects ||--o{ skill_benchmarks : runs
    projects ||--o{ marketplace_cache : caches

    github_links ||--o{ github_events : receives

    invites }o--|| users : "consumed by"
    subscriptions ||--o{ stripe_events : "logs idempotency"

    users {
        TEXT id PK
        TEXT email UK
        TEXT password_hash
        TEXT role "admin · user"
        TEXT google_id UK
        TEXT github_id UK
        INTEGER email_verified
        INTEGER two_factor_enabled
        TEXT last_login
    }

    projects {
        INTEGER id PK
        TEXT technical_name
        TEXT owner_id FK
        TEXT display_name
        INTEGER is_archived
        TEXT model_mode "strict|optimize (#520, migration 060)"
    }

    chat_messages {
        INTEGER id PK
        TEXT project_name
        TEXT worker_id
        TEXT role "user · assistant · system"
        TEXT content "AES-256-GCM if encrypted=1"
        INTEGER encrypted
        TEXT timestamp
    }

    subscriptions {
        TEXT user_id PK
        TEXT plan "free · min · max · beta"
        TEXT stripe_customer_id
        TEXT status
    }

    invites {
        TEXT code PK "arc-XXXX-XXXX"
        TEXT created_by FK
        TEXT used_by FK
        TEXT parent_invite_code
        TEXT status "active · used · revoked"
    }

    recovery_keys {
        INTEGER id PK
        TEXT user_id FK
        TEXT encrypted_key
        TEXT key_hint "first 4 chars"
    }

    trial_credits {
        TEXT email PK
        INTEGER credits_used
        TEXT first_used_at
    }

Plus de 30 tables réparties sur 35 migrations. Auth + projets + données de workspace + intelligence + facturation + gating bêta + audit de context-export. Toutes les contraintes FK appliquées.

Tables

users — Utilisateurs

Colonne Type Description
id TEXT PK UUID
email TEXT UNIQUE Email (requis)
password_hash TEXT Hash du mot de passe (bcrypt)
role TEXT 'admin' ou 'user'
name TEXT Nom
avatar_url TEXT URL de l'avatar
google_id TEXT UNIQUE ID Google OAuth
github_id TEXT UNIQUE ID GitHub OAuth
email_verified INTEGER 0/1
two_factor_enabled INTEGER 0/1
totp_secret TEXT Secret TOTP (2FA)
last_login TEXT ISO 8601
created_at TEXT ISO 8601
password_version INTEGER Compteur de changements de mot de passe (migration 035). Inclus dans le JWT comme pv ; crmAuthMiddleware rejette les tokens périmés après un changement de mot de passe.

projects — Projets

Colonne Type Description
id INTEGER PK Auto
technical_name TEXT Nom technique (unique par propriétaire)
owner_id TEXT FK→users Propriétaire
display_name TEXT Nom d'affichage
description TEXT Description
color TEXT Couleur (hex)
icon TEXT Icône
is_archived INTEGER 0/1
sort_order INTEGER Ordre de tri (par défaut 999)
project_protocol TEXT Protocole du projet
created_at, updated_at TEXT Horodatages

UNIQUE(technical_name, owner_id)

account_settings — Paramètres du compte

Colonne Type Description
user_id TEXT PK FK→users
anthropic_key TEXT Clé API Anthropic
openai_key TEXT Clé API OpenAI
name TEXT Nom
role TEXT Rôle
auto_harvest INTEGER Auto-harvesting de skills 0/1

chat_messages — Historique du chat

Colonne Type Description
id INTEGER PK Auto
project_name TEXT Projet
worker_id TEXT Worker
role TEXT 'user', 'assistant', 'system'
content TEXT Texte du message (chiffré AES-256-GCM quand encrypted=1)
encrypted INTEGER 0=texte clair, 1=chiffré-vault (Phase 45.3)
attachments TEXT Tableau JSON
timestamp TEXT ISO 8601
metadata TEXT Objet JSON

INDEX(project_name, worker_id, timestamp DESC)

recovery_keys — Clés de récupération (Phase 45.4)

Colonne Type Description
id INTEGER PK Auto
user_id TEXT FK→users Propriétaire
encrypted_key TEXT Clé maître chiffrée
key_hint TEXT Indice (4 premiers caractères)
created_at TEXT ISO 8601
used_at TEXT Quand elle a été utilisée
revoked INTEGER 0=active, 1=révoquée

INDEX(user_id)

github_links — Liaisons de repos GitHub (Phase 49.3)

Colonne Type Description
id INTEGER PK Auto
project_name TEXT Projet Arc OS
owner TEXT Propriétaire du repo GitHub
repo TEXT Nom du repo GitHub
webhook_secret TEXT UNIQUE Hex de 32 octets pour la validation HMAC-SHA256
created_at TEXT ISO 8601
created_by TEXT FK→users Qui a créé le lien

UNIQUE(project_name, owner, repo) — multi-repo par projet autorisé. INDEX(project_name), INDEX(webhook_secret)

github_events — Log d'événements GitHub (Phase 49.3.1)

Colonne Type Description
id INTEGER PK Auto
link_id INTEGER FK→github_links CASCADE à la suppression du lien
project_name TEXT Projet Arc OS (dénormalisé pour la vitesse de requête)
event_type TEXT push, pull_request, workflow_run, issues
action TEXT sous-action (opened/closed/success/failure)
summary TEXT Chaîne d'affichage pré-formatée
url TEXT Lien profond GitHub
actor TEXT Username GitHub
created_at TEXT ISO 8601

INDEX(project_name, created_at DESC), INDEX(link_id)

skills_global — Skills globaux

Colonne Type Description
id INTEGER PK Auto
name TEXT UNIQUE Nom
description TEXT Description
category TEXT Catégorie (par défaut 'general')
content TEXT Contenu du skill (markdown)
triggers TEXT Tableau JSON de triggers
keywords TEXT Tableau JSON de keywords
eval_rules TEXT Tableau JSON de règles
tool_code_ts TEXT Implémentation TypeScript
version INTEGER Version (auto-incrément)
status TEXT 'active', 'draft', 'deprecated', 'archived'
owner_project TEXT NULL=global/marketplace ; nom de projet=visible uniquement par le propriétaire (migration 034)

skills_project_forks — Forks de skills

Colonne Type Description
id INTEGER PK Auto
project_name TEXT Projet
skill_id INTEGER FK→skills_global Skill parent (CASCADE)
content TEXT Contenu personnalisé
triggers, keywords, eval_rules, tool_code_ts TEXT Overrides
version INTEGER Version du fork

UNIQUE(project_name, skill_id)

skill_evolution_logs — Historique des changements de skills

Colonne Type Description
id INTEGER PK
skill_id INTEGER FK CASCADE
project_name TEXT Projet (nullable)
action TEXT 'created', 'modified', 'applied', 'reverted'
diff_summary TEXT Description des changements
author TEXT Auteur (par défaut 'system')
metadata TEXT JSON

skill_update_requests — PRs de skills (Sage)

Colonne Type Description
id INTEGER PK
skill_id INTEGER FK CASCADE
proposed_by TEXT 'sage' (par défaut)
status TEXT 'pending', 'approved', 'rejected', 'applied'
current_content TEXT Version courante
proposed_content TEXT Version proposée
reason TEXT Raison du changement

skill_benchmarks — Tests A/B de skills

Colonne Type Description
id INTEGER PK
request_id INTEGER FK→skill_update_requests CASCADE
test_scenario TEXT Scénario de test
old_output, new_output TEXT Résultats
score_old, score_new REAL Scores
judgment_reason TEXT Justification

pinned_notes — Notes épinglées

Colonne Type Description
id INTEGER PK
project_name TEXT Projet
worker_id TEXT Worker
title TEXT Titre
body TEXT Texte
source_message_id INTEGER Référence vers chat_messages

project_phases — Phases de roadmap

Colonne Type Description
id INTEGER PK
project_name TEXT Projet
phase_id INTEGER Numéro de phase
phase_title TEXT Titre
status TEXT 'PLANNED', 'ACTIVE', 'COMPLETED', 'CANCELLED'
progress REAL 0.0–1.0
deadline TEXT ISO 8601

UNIQUE(project_name, phase_id)

activity_log — Log d'activité

Colonne Type Description
id INTEGER PK
project_name TEXT Projet
actor TEXT Worker ou utilisateur
event_type TEXT Type d'événement
title TEXT Description
metadata TEXT JSON
created_at TEXT ISO 8601

INDEX(created_at DESC), INDEX(project_name, created_at DESC), INDEX(event_type, created_at DESC), INDEX(actor, created_at DESC)

system_configs — Paramètres système

Colonne Type Description
key TEXT PK Clé
value TEXT Valeur
description TEXT Description

marketplace_analysis_cache — Cache du marketplace

Cache de l'analyse de compatibilité des skills du marketplace.

ephemeral_tokens — Tokens éphémères (issue #27)

Store persistant pour les tokens à usage unique : OAuth state, reset de mot de passe, vérification d'email. Avant la migration 022, ces tokens vivaient dans une Map en mémoire et étaient perdus à chaque redémarrage du master, cassant le login Google/GitHub (invalid_state).

Colonne Type Description
token TEXT PK Hex de 32 octets (randomBytes(32))
type TEXT NOT NULL oauth_state | password_reset | email_verification
payload TEXT NOT NULL JSON : {provider} pour oauth_state, {email} pour le reste
expires_at INTEGER NOT NULL epoch ms — TTL : 10 min (oauth) / 30 min (reset) / 24 h (verify)

INDEX(type), INDEX(expires_at). Usage unique : consume() lit le payload et supprime la ligne dans une seule transaction.

project_issues — Issues par projet (issue #53, Phase 53.14)

SSOT SQLite pour les issues, remplaçant le issues/issues.json par projet. Le fichier reste sur disque comme export dérivé (miroir), gitignoré — la DB fait autorité.

Colonne Type Description
project_name TEXT NOT NULL nom du registry ; fait partie de la PK composite avec id
id INTEGER NOT NULL séquence par projet en base 1 (max+1 à l'insertion)
title TEXT NOT NULL titre de l'issue
body TEXT NOT NULL DEFAULT '' description, markdown
priority TEXT NOT NULL DEFAULT 'P2' P0 | P1 | P2 | P3
labels TEXT NOT NULL DEFAULT '[]' tableau JSON de chaînes
status TEXT NOT NULL DEFAULT 'open' draft | open | in_progress | blocked | deferred | closed (le défaut SQL 'open' est conservé — les nouvelles issues sont forcées en draft au niveau applicatif)
assignee TEXT NULL id du worker qui a pris l'issue (migration 036)
created_by TEXT NOT NULL DEFAULT 'legacy' id du worker qui a créé l'issue (migration 046 / #327) — pour l'audit (qui, quand, assigné à qui)
created_at TEXT NOT NULL horodatage ISO
updated_at TEXT NOT NULL horodatage ISO
closed_at TEXT NULL horodatage ISO quand status=closed
activity TEXT NOT NULL DEFAULT '[]' tableau JSON {ts, type, author, text} — audit append-only

PK (project_name, id) — chaque projet a son propre espace d'id indépendant. INDEX (project_name, status) — le filtre le plus fréquent (open uniquement). Les opérations vivent dans issueQueries (shared/db.ts) : list/get/nextId/insert/upsert/replaceAll/bulkImport. replaceAll — transaction atomique ; bulkImportINSERT OR IGNORE pour un re-seed idempotent.

Migration 036 (#187, 2026-05-23) : ALTER TABLE project_issues ADD COLUMN assignee TEXT. Les nouveaux statuts (in_progress, blocked, deferred) sont validés au niveau applicatif (union IssueStatus dans shared/db.ts), pas via une contrainte CHECK (SQLite ne permet pas d'ajouter des contraintes via ALTER TABLE).

Migration 046 (#327, 2026-06-03) : ALTER TABLE project_issues ADD COLUMN created_by TEXT NOT NULL DEFAULT 'legacy'. Le cycle de vie est étendu d'un nouveau statut draft. handleCreateIssue force status='draft' quand assignee=null (sinon immédiatement 'open'), et exige worker_id (body ou fallback auth). handleUpdateIssue promeut automatiquement draft → open à la première assignation d'assignee et rejette close pour une issue jamais assignée (FR-ISS-101/102/103). Les lignes préexistantes sont remplies via DEFAULT comme 'legacy'.

onboarding_progress — État de la checklist post-assistant (issue #56, Phase 54.1)

Cache dérivé par utilisateur au-dessus du flux d'événements activity_log (SSOT). L'UI le lit avec une seule requête au lieu d'agréger sur les événements. Rejouable : dismiss/replay ne changent pas state, seulement dismissed_at.

Colonne Type Description
chat_id TEXT PRIMARY KEY id utilisateur (chat id du registry)
state TEXT NOT NULL DEFAULT '{}' JSON {workers, cli, skill, bot, issue}completed | skipped (clés absentes = pending)
completed_count INTEGER NOT NULL DEFAULT 0 cache dérivé du nombre d'étapes completed (0–5)
started_at TEXT NOT NULL DEFAULT (datetime('now')) création de la ligne (premier event/dismiss/replay)
completed_at TEXT NULL horodatage ISO quand les 5 étapes sont completed
dismissed_at TEXT NULL horodatage ISO quand l'utilisateur a fermé la checklist
source TEXT NOT NULL DEFAULT 'web' web | cli — pour l'attribution du funnel (#58 arc tour)
updated_at TEXT NOT NULL DEFAULT (datetime('now')) horodatage de mutation

INDEX sur completed_at pour les analytics de funnel (#61 Phase 54.6). Les opérations vivent dans onboardingQueries (shared/db.ts) : getProgress/recordEvent/dismiss/replay. recordEvent est idempotent sur (chat_id, step, status) — un appel identique répété renvoie changed=false et n'écrit pas dans activity_log. Whitelist : 5 étapes × 2 statuts (completed, skipped). skipped n'incrémente PAS completed_count. La transition skipped → completed ajoute au compte. Toutes les mutations émettent des événements dans activity_log avec event_type LIKE 'onboarding_%' — la vraie SSOT pour les métriques de funnel ; les colonnes de la table sont un cache dérivé.

platform_audit_log — Trail de rotation des secrets super-admin (Phase 57, Sentinel #103)

Log append-only des mutations sur les secrets de plateforme du vault via l'UI Platform Settings. SSOT pour la forensique post-incident (« quel admin a tourné la clé Anthropic à 03:14 UTC ? »). Le vault.json lui-même n'enregistre pas cela — value-only.

Colonne Type Description
id INTEGER PRIMARY KEY AUTOINCREMENT ordre temporel même en cas de dérive d'horloge
ts TEXT NOT NULL DEFAULT (datetime('now')) UTC côté serveur ; un attaquant ne peut pas antidater
user_chat_id TEXT NOT NULL admin qui a agi
user_email TEXT NULL snapshot de la table users au moment de l'action
action TEXT NOT NULL list | view | rotate | test | restart
key_name TEXT NOT NULL nom de l'entrée vault (allowlist dans platform.ts) ; * pour list-all
ip TEXT NULL depuis CF-Connecting-IP / X-Real-IP / queue XFF
result TEXT NOT NULL success ou fail:<reason>
user_agent TEXT NULL chaîne UA, tronquée à 200 caractères

INDEX : idx_platform_audit_ts (récents sur toutes les clés), idx_platform_audit_user (trail par admin), idx_platform_audit_key (historique de rotation par clé). Invariant append-only : aucun handler UPDATE/DELETE ; l'UI expose recent() + lastRotated() en lecture seule via platformAuditQueries dans shared/db.ts. Chaque action (succès+échec) écrit une ligne.

auth_events — Log d'événements d'authentification utilisateur (Migration 027, issue #130)

Log append-only des événements d'auth pour l'audit de sécurité et l'investigation d'incidents. Distinct de platform_audit_log (vault admin uniquement) — cette table couvre le flow d'auth de l'utilisateur final avec IP.

Colonne Type Description
id INTEGER PRIMARY KEY AUTOINCREMENT ordre temporel
ts TEXT NOT NULL DEFAULT (datetime('now')) UTC côté serveur
event TEXT NOT NULL signup | login | oauth_login | magic_link | device_code_approve
provider TEXT NOT NULL DEFAULT 'password' password | google | github | magic_link | device_code
user_id TEXT NULL NULL pour les tentatives échouées où l'utilisateur n'est pas identifié
email TEXT NULL email utilisé dans la tentative
ip TEXT NOT NULL DEFAULT 'unknown' CF-Connecting-IP → X-Real-IP → dernier segment XFF
user_agent TEXT NULL chaîne UA
result TEXT NOT NULL success | failed | rate_limited | invalid_invite | already_registered | 2fa_required | invalid_token
meta TEXT NULL contexte JSON supplémentaire (ex. {isNew: true} pour OAuth)

INDEX : idx_auth_events_ts (plus récents d'abord), idx_auth_events_user (historique par utilisateur), idx_auth_events_ip (détection de menace par IP), idx_auth_events_event (filtre event+result). authEventQueries.insert/recent dans shared/db.ts. Loggé dans master-bot/routes/auth.ts (register/login/oauth/magic-link) et master-bot/routes/cli.ts (device_code_approve).

Migrations

# Nom Phase Ce qu'elle ajoute
001 initial_schema Phase 40 users, projects, account_settings
002 project_protocol Phase 40.7 projects.project_protocol
003 chat_messages Phase 40.10 chat_messages
004 skill_system Phase 41 skills_global, skills_project_forks, skill_evolution_logs, skill_update_requests
005 skill_benchmarks Phase 40.11d skill_benchmarks
006 system_configs system_configs + FIGMA_CACHE_ROOT
007 marketplace_cache Phase 40.12.1 marketplace_analysis_cache
008 harvester Phase 40.13 +is_suggestion, +source_query, +auto_harvest
009 pinned_notes Phase 41.8 pinned_notes
010 project_phases Phase 44 project_phases
011 activity_log Phase 44.1 activity_log
012 performance_indexes Perf Audit +idx_activity_log_actor, +idx_chat_msg_role_ts
013 timeline_timestamps Phase 47 +timestamp sur les messages des workers
014 timeline_events Phase 47 table timeline_events
015 e2ee_chat Phase 45.3 +colonne encrypted sur chat_messages
016 recovery_keys Phase 45.4 table recovery_keys
017 github_links Phase 49.3 table github_links (liaisons de webhook)
018 github_events Phase 49.3.1 table github_events (log d'événements pour le feed UI)
019 trial_credits Phase 50.1 +users.trial_granted_at, +projects.trial_mode, +projects.trial_tokens_remaining
020 subscriptions Phase 51 table subscriptions (plan, stripe_customer_id, status, feature_flags) + log d'idempotence stripe_events
021 invites Phase 52.1 table invites (enrôlement bêta fermée) — code, created_by, used_by, parent_invite_code, status, note
022 ephemeral_tokens issue #27 table ephemeral_tokens — OAuth state / reset de mot de passe / vérification d'email persistants, remplaçant la Map en mémoire (corrige invalid_state au redémarrage du master)
023 project_issues Phase 53.14 / issue #53 table project_issues (PK composite project_name+id, labels/activity JSON) — déplace le stockage des issues de issues/issues.json vers la SSOT SQLite, le JSON devient un miroir dérivé ; élimine la dérive de merge de texte au déploiement
024 context_exports Phase 56.4 + 56.5 / issues #100 + #101 export_audit_log (id PK, project_name, owner_id, exported_at, sections JSON, findings_critical/high/medium/low INT, bytes) + export_preferences (project_name PK, always_include_emails BOOL, auto_redact_critical BOOL DEFAULT 1, notify_on_export BOOL DEFAULT 0, updated_at) — historique d'audit par projet pour Project Context Export + toggles de politique par projet ; alerte à ≥3 exports/24h quand notify_on_export = true
025 onboarding_progress Phase 54.1 / issue #56 onboarding_progress (chat_id PK, JSON state par 5 étapes, completed_count dérivé, started_at/completed_at/dismissed_at, source web|cli) — cache dérivé au-dessus du flux d'événements activity_log ; SSOT = événements avec event_type LIKE 'onboarding_%' ; recordEvent idempotent, dismiss/replay non destructifs
026 platform_audit_log Phase 57 / Sentinel #103 platform_audit_log (id PK AUTOINCREMENT, ts, user_chat_id, user_email, action, key_name, ip, result, user_agent) — trail d'audit append-only pour les mutations de l'UI Platform Settings super-admin sur les secrets du vault. 3 index : idx_platform_audit_ts/user/key. Pas d'UPDATE/DELETE — lecture seule via platformAuditQueries.recent + lastRotated
027 auth_events issue #130 auth_events (id PK AUTOINCREMENT, ts, event, provider, user_id, email, ip, user_agent, result, meta JSON) — log append-only IP + événements d'auth. Événements : signup/login/oauth_login/magic_link/device_code_approve. Résultats : success/failed/rate_limited/invalid_invite/already_registered/2fa_required/invalid_token. 4 index : ts/user_id/ip/event+result
028 managed_containers Phase 60 #135/#136 managed_containers (id TEXT PK (nom du conteneur docker), user_id, status (provisioning|ready|paused|suspended|deleted), server_ip, internal_port INT, claude_authed INT, github_authed INT, created_at, last_active) — registry de conteneurs Docker 1-par-utilisateur pour Standard Cloud. managedContainerQueries.findByUser/insert/updateStatus/setAuthFlag/touch/listActive dans shared/db.ts. Endpoints : POST /api/crm/cloud/provision, GET /status, POST /deprovision dans shared/routes/cloud.ts
029 cloud_waitlist Phase 60 #134 cloud_waitlist (id INT PK AUTOINCREMENT, user_id TEXT UNIQUE, email, position INT, status (waiting|invited|activated|declined), notes, joined_at, invited_at, activated_at) — waitlist Early Access pour Standard Cloud.
030 translation_feedback Phase 59.4 #122 translation_feedback (id INT PK AUTOINCREMENT, locale, message_id, original, current_translation, suggestion, user_id, status (pending|accepted|rejected), created_at, reviewed_at).
031 token_usage_log Phase 63 #148 token_usage_log (id INT PK AUTOINCREMENT, project_name, owner_id, worker_id, input_tokens, output_tokens, cache_tokens, total_tokens, created_at INT) — Utilisation des tokens Claude par requête. Rempli par claude-runner.ts via /api/internal/usage/log.
032 arc_help_usage Phase 61 #147 arc_help_usage (user_id, date PK composite) — Compteur de rate-limit quotidien pour Arc Help (30 msg/jour).
033 arc_help_messages Phase 61 #153 arc_help_messages (id PK AUTOINCREMENT, user_id, role, text, sources JSON, created_at) — Historique de chat Arc Help persistant par utilisateur (60 derniers).
034 skills_owner_project #157 ALTER TABLE skills_global ADD COLUMN owner_project TEXT — NULL=global, non-null=possédé par un projet.
035 password_version Phase 64 #174 ALTER TABLE users ADD COLUMN password_version INTEGER DEFAULT 0 — incrémenté au changement de mot de passe ; claim JWT pv validé par crmAuthMiddleware pour invalider les anciens tokens.
036 issue_assignee #187 / 2026-05-23 ALTER TABLE project_issues ADD COLUMN assignee TEXT — id de worker nullable. Active arc issue take <id> + les statuts étendus in_progress/blocked/deferred (validés au niveau applicatif).
046 issue_draft_and_created_by #327 / 2026-06-03 ALTER TABLE project_issues ADD COLUMN created_by TEXT NOT NULL DEFAULT 'legacy' — champ d'audit « qui l'a créée ». Cycle de vie += draft (union au niveau applicatif). handleCreateIssue force draft sans assignee ; handleUpdateIssue promeut automatiquement draft→open à l'assignation + rejette close pour jamais-assignée (FR-ISS-101/102/103).
037 waitlist #197 / 2026-05-25 waitlist (id INT PK AUTOINCREMENT, email TEXT UNIQUE, message, status DEFAULT 'pending', created_at, updated_at) — formulaire de demande d'accès anticipé sur la page de login.
038 plata_billing Phase 65 #202 / 2026-05-26 Ajoute les colonnes Plata by mono à subscriptions : plata_card_token TEXT, plata_wallet_id TEXT, plata_masked_pan TEXT, next_billing_date TEXT, billing_failures INTEGER DEFAULT 0. Crée aussi la table d'idempotence plata_events (invoice_id PK, status, processed_at).
039 skill_project_grants #201 / 2026-05-26 skill_project_grants (skill_id INT FK→skills_global.id CASCADE, project_name TEXT, granted_at, PK composite) — accès aux skills many-to-many entre projets.
040 drop_stripe #205 / 2026-05-26 DROP de la colonne stripe_customer_id + DROP TABLE stripe_events. NB : la colonne stripe_subscription_id a survécu à cette migration car SQLite refuse DROP COLUMN sur les colonnes contraintes UNIQUE (silencieusement attrapé par try/catch). Voir migration 041.
041 rebuild_subscriptions #205 / 2026-05-26 Suite de 040 — reconstruit la table subscriptions sans la colonne orpheline stripe_subscription_id via le pattern CREATE TABLE … AS. Idempotent (sauté quand la colonne est déjà absente).
042 totp_backup_codes ALTER TABLE users ADD COLUMN totp_backup_codes TEXT — codes de récupération TOTP.
043 worker_avatars Phase D #304 / 2026-05-30 worker_avatars (id PK AUTOINCREMENT, project_name, worker_id, file_path, mime_type, created_at; UNIQUE(project_name, worker_id)) — store de métadonnées pour les images d'avatar de worker uploadées. Fichiers enregistrés dans data/worker-avatars/. Servi via GET /api/crm/projects/:name/workers/:id/avatar.
044 worker_templates Phase I #304 / 2026-05-30 worker_templates (id PK AUTOINCREMENT, owner_id, name, description, config JSON, is_public, created_at; UNIQUE(owner_id, name)) — templates de worker enregistrés par l'utilisateur. Endpoints : GET/POST /api/crm/workers/templates, DELETE /api/crm/workers/templates/:id.
045 workers_runtime_state Phase F #306 / 2026-05-31 workers_runtime_state (project_name, worker_id, status, status_started_at, current_skill, current_task_id, updated_at; PK(project_name, worker_id), idx sur updated_at) — état runtime live par worker. Écrit par handleSetActiveRole (invariant un seul rôle actif par projet). Lu par GET /api/crm/projects/:name/workers/:id/runtime avec fallback de péremption : status='working' + tmux mort + updated_at > 10 min → forcé à idle (détection de crash).
049 embeddings Phase 71.2 #359 / 2026-06-04 embeddings (id PK AUTOINCREMENT, project, doc_type wiki/issue/skill, doc_id, chunk_ix, text, created_at) + embeddings_vec (table virtuelle vec0, embedding FLOAT[1024]). Split : vec0 ne peut pas contenir efficacement de métadonnées ; les workflows LIST/DELETE tournent en SQL classique. Extension sqlite-vec auto-chargée dans initDb avant l'exécution de la migration.
050 drop_notebook_id Phase 71.8 #365 / 2026-06-05 ALTER TABLE projects DROP COLUMN notebook_id. Décommissionne le champ de schéma du bridge NotebookLM ; la recherche sémantique passe désormais par le RAG auto-hébergé (Cohere + sqlite-vec). Idempotent via un check PRAGMA table_info pour que les DB fraîches avec le schéma 001 courant ne trébuchent pas sur une colonne manquante.
051 voice_usage Phase 62.4 #373 / 2026-06-05 voice_usage_log (user_id TEXT, date TEXT YYYY-MM-DD, seconds_used INTEGER DEFAULT 0, PRIMARY KEY(user_id, date)) — compteur de quota quotidien de transcription vocale, reflète la forme de la migration 032 arc_help_usage. Secondes approximatives (estimées depuis la taille en octets de l'upload en supposant un codec voix ~32 kbps) ; comparées à un plafond souple de 60 min/jour dans handleVoiceTranscribe (shared/routes/voice.ts) avant chaque transcribe pour empêcher un utilisateur de monopoliser le pool de workers whisper-server partagé sur Contabo.
052 transcripts Phase 73.1 #377 / 2026-06-05 transcripts (id PK AUTOINCREMENT, project TEXT, owner_id TEXT, source_filename TEXT, source_bytes INTEGER, source_kind TEXT CHECK(audio|video), duration_seconds INTEGER, transcript_text TEXT, frames_json TEXT, summary_json TEXT, embed_to_rag INTEGER DEFAULT 1, status TEXT DEFAULT 'queued', error TEXT, created_at TEXT DEFAULT datetime('now'), completed_at TEXT) — une ligne par fichier de réunion uploadé. frames_json = tableau JSON de {ts_ms, description} de Claude vision (Phase 73.4). summary_json = {tldr, key_points, action_items, decisions, topics, model, generated_at} de Claude Sonnet (Phase 73.5). Fichier source supprimé du disque une fois status='done' (décision CEO D4). Index sur (project, created_at DESC) et (owner_id, created_at DESC).
056 skill_usage #319 / 2026-06-07 skill_usage (skill_name TEXT, project_name TEXT DEFAULT '_global_', trigger_count INTEGER DEFAULT 0, install_count INTEGER DEFAULT 0, session_count INTEGER DEFAULT 0, last_triggered_at TEXT, updated_at TEXT, PRIMARY KEY(skill_name, project_name)) — compteurs cumulatifs par skill par projet. trigger_count s'incrémente à chaque injection du skill par le context router ; install_count à la sauvegarde/création ; session_count à l'apparition dans une session unique. Utilisé par GET /api/crm/projects/:name/skills/usage.
057 notes Phase 78.1 #395 / 2026-06-07 4 tables : notes (id PK, project_name, title, description, created_by, created_at, updated_at) — une note comme conteneur de sources. note_sources (id PK, note_id FK→notes, source_type CHECK(video|audio|youtube|web|pdf|docx|txt|image), filename, file_path, file_size, url, title, content_text, duration_seconds, status CHECK(queued|processing|done|error), error, processed_at, created_at) — une source par ligne, content_text = texte extrait pour le RAG. note_issue_links (note_id FK, issue_id, project_name, linked_at, PK(note_id,issue_id)) — lien note↔issue. note_chats (id PK, note_id FK→notes, role CHECK(user|assistant), content, created_at) — historique de chat avec la note. Index : notes(project_name,created_at DESC), note_sources(note_id), note_sources(status), note_issue_links(project_name,issue_id), note_chats(note_id,created_at).
053 transcript_jobs Phase 73.1 #377 / 2026-06-05 transcript_jobs (job_id TEXT PK, transcript_id INTEGER, status TEXT DEFAULT 'queued', progress_pct INTEGER DEFAULT 0, step_label TEXT, error TEXT, started_at TEXT DEFAULT datetime('now'), updated_at TEXT DEFAULT datetime('now')) — tracker de progression à fort taux de churn pour le worker d'arrière-plan. Isolé de transcripts pour que les UPDATE fréquents (toutes les quelques secondes pendant ffmpeg/whisper) ne remuent pas les index de la table principale. L'endpoint SSE streame cette table à un poll de 1s. Cycle de vie du statut : queued → extracting_audio → transcribing → (video: extracting_frames → frames_extracted → vision_analyzing → vision_analyzed) → summarizing → summarized → embedding → done (ou failed à n'importe quelle étape). Index sur (status, updated_at).

Relations (clés étrangères)

Tout en CASCADE — supprimer un skill retire tous ses forks, historique, PRs et benchmarks.

Diagramme ER (textuel)

users ─1:N──> projects
            ├── chat_messages
            ├── pinned_notes
            ├── project_phases
            └── skills_project_forks ──> skills_global
                                          ├── skill_evolution_logs
                                          ├── skill_update_requests
                                          │    └── skill_benchmarks
                                          └── (standalone tables)
                                               ├── activity_log
                                               ├── system_configs
                                               └── marketplace_analysis_cache

| 029 | cloud_waitlist | Phase 60 #134 | cloud_waitlist (id INT PK AUTOINCREMENT, user_id TEXT UNIQUE, email, position INT, status (waiting|invited|activated|declined), notes, joined_at, invited_at, activated_at) — waitlist Early Access pour Standard Cloud. cloudWaitlistQueries.join/findByUser/list/invite/activate/decline/stats dans shared/db.ts. Endpoints : POST /api/crm/cloud/waitlist (join), GET /waitlist/status (own de l'utilisateur), GET /waitlist (liste admin), POST /waitlist/invite (activation admin + passe le plan à cloud) | | 030 | translation_feedback | Phase 59.4 #122 | translation_feedback (id INT PK AUTOINCREMENT, locale TEXT, message_id TEXT, original TEXT, current_translation TEXT, suggestion TEXT, user_id TEXT, status (pending|accepted|rejected), created_at, reviewed_at) — feedback de qualité de traduction par locale. Dashboard de review admin à /api/crm/translations/feedback (list/stats/accept/reject). | | 031 | token_usage_log | Phase 63 #148 | token_usage_log (id INT PK AUTOINCREMENT, project_name TEXT NOT NULL, owner_id TEXT NOT NULL, worker_id TEXT, input_tokens INT DEFAULT 0, output_tokens INT DEFAULT 0, cache_tokens INT DEFAULT 0, total_tokens INT DEFAULT 0, created_at INT DEFAULT (unixepoch())) — log d'utilisation des tokens Claude par requête. 3 index : owner_id, project_name, created_at. Rempli par child-bot/claude-runner.ts via POST /api/internal/usage/log (fire-and-forget). Interrogé par GET /api/crm/account/usage → renvoie { rows: [...last 200], totals: { total, input, output } } pour BillingPage + carte d'usage du UserDropdown. | | 032 | arc_help_usage | Phase 61 #147 | arc_help_usage (id INT PK AUTOINCREMENT, user_id TEXT NOT NULL, date TEXT NOT NULL (YYYY-MM-DD), count INT NOT NULL DEFAULT 0, PRIMARY KEY (user_id, date)) — compteur de rate-limit quotidien pour le chat d'aide IA in-app (30 messages/jour). Interrogé par GET /api/crm/help/usage ; mis à jour sur POST /api/crm/help/chat. | | 033 | arc_help_messages | Phase 61 #153 | arc_help_messages (id INT PK AUTOINCREMENT, user_id TEXT NOT NULL, role TEXT NOT NULL CHECK(user|assistant), text TEXT NOT NULL, sources TEXT (tableau JSON, nullable), created_at TEXT DEFAULT datetime('now')) — historique de chat Arc Help persistant par utilisateur. INDEX sur (user_id, created_at DESC). arcHelpQueries.save/history(limit=60)/clearHistory dans shared/db.ts. GET /api/crm/help/history renvoie les 60 derniers messages (plus anciens d'abord). DELETE /api/crm/help/history efface tout pour l'utilisateur. Le frontend charge au montage. | | 034 | skills_owner_project | #157 | ALTER TABLE skills_global ADD COLUMN owner_project TEXT DEFAULT NULL — NULL=global/marketplace (visible par tous), non-null=possédé par ce projet (filtré dans listForProject). Backfill : les skills à catégorie non générique (odoo, python, etc.) reçoivent owner_project=category. Catégories génériques : general/frontend/backend/devops/security/testing/database/api/mobile. Les skills DB sont désormais injectés dans les prompts Claude via routeContextFromDb() dans le child-bot (#158). |

Migration 064 — token_usage_log.model (#562 S5)

Colonne nullable model TEXT sur token_usage_log : quel modèle Claude a servi la ligne d'usage (propagé depuis claude-runner via POST /api/internal/usage/log). Alimente le split de tokens par tier dans GET /api/crm/analytics/cascade pour que l'effet coût de la cascade gated par spec soit mesurable. Les lignes legacy restent NULL (unattributed).

Migration 067 — cli_devices (#627, Phase B de #617)

cli_devices (id INT PK AUTOINCREMENT, chat_id TEXT NOT NULL, device_id TEXT NOT NULL, hostname, platform, arc_version, last_project, first_seen, last_seen; UNIQUE(chat_id, device_id); INDEX idx_cli_devices_chat (chat_id, last_seen DESC)) — registry côté serveur des installations arc CLI liées, une ligne par (utilisateur, appareil). device_id est un hex aléatoire de 12 octets généré côté client (arc ≥1.0.14) et persisté dans ~/.arc/config.json. Écrit par POST /api/cli/heartbeat (à chaque invocation arc + juste après arc login) et optionnellement par device/poll quand le client envoie des infos client{}. La PREMIÈRE ligne d'appareil de l'utilisateur logge aussi activity_log(cli_invocation, event=first_device_linked) pour que la sonde cli-status de l'onboarding et le funnel d'activation enregistrent la liaison. Lu par GET /api/crm/cli/devices (cliDeviceQueries.upsert/listForUser dans shared/db.ts) → liste « Connected devices » dans la modale CLI de la sidebar (style Tailscale : hostname · platform · version · last seen).

Migration 068 — account_settings.hide_browser_chat (#630, Phase E de #617)

hide_browser_chat INTEGER DEFAULT 0 (nullable-default) sur account_settings — feature flag par utilisateur qui masque les surfaces de chat navigateur (item de nav du Workspace + la page par défaut du projet devient le feed Sessions en lecture seule). Par défaut 0 = chat visible, rien ne change ; le rebasculer restaure l'ancienne UI à l'identique (aucun code de chat supprimé). Exposé comme hideBrowserChat dans GET/PUT /api/crm/account/settings ; accountQueries.getHideBrowserChat/setHideBrowserChat dans shared/db.ts.

Migration 070 — account_settings.calm_mode + heures calmes (#644, T8)

Ajoute calm_mode INTEGER DEFAULT 0 + quiet_start / quiet_end (heures INTEGER 0–23, nullable) à account_settings. Calm mode = supprime les toasts success/info du centre (les erreurs apparaissent toujours) ; les événements significatifs restent dans le feed Home + la cloche de l'en-tête. Heures calmes = une fenêtre [start,end) (qui enjambe minuit) durant laquelle les toasts non-erreur sont silencieux même si le calm mode est off. Le gating est côté client (ToastViewport lit un miroir localStorage['arc-calm'] que App écrit depuis ces paramètres) ; les colonnes ne font que persister la préférence. Exposé comme calmMode/quietStart/quietEnd dans GET/PUT /api/crm/account/settings ; accountQueries.getCalm/setCalm.

Migration 069 — cli_transcripts + account_settings.upload_transcripts (#632)

cli_transcripts (id INT PK AUTOINCREMENT, chat_id TEXT NOT NULL, project_name TEXT NOT NULL, session_id TEXT NOT NULL, worker_id, device_id, message_count INT, first_prompt TEXT, size_bytes INT, content BLOB NOT NULL, created_at, updated_at; UNIQUE(chat_id, session_id); INDEX idx_cli_transcripts_lookup (chat_id, project_name, updated_at DESC)) — stockage serveur opt-in des transcripts complets de sessions CLI pour la continuité cross-device. content est le jsonl brut gzippé (Bun.gzipSync) ; size_bytes est la taille non compressée. Écrit par POST /api/cli/transcript/:project/:mode (arc ≥1.0.16) uniquement quand le propriétaire a activé le flag ; upsert sur (chat_id, session_id) pour qu'une session grandissante écrase. Lu, limité au propriétaire, par GET /api/crm/projects/:name/cli-transcript?session=… (cliTranscriptQueries dans shared/db.ts). Ajoute aussi upload_transcripts INTEGER DEFAULT 0 à account_settings — le flag opt-in (par défaut OFF ; les transcripts peuvent contenir du code/secrets), exposé comme uploadTranscripts dans GET/PUT /api/crm/account/settings et renvoyé au CLI dans le bloc settings de /api/cli/init. GDPR Art.17 : cli_transcripts + cli_devices sont supprimés par chat_id dans la cascade d'effacement.