Schéma BDD — Telaria
Source de vérité : entités Doctrine PHP de chaque bundle. Ce document est dérivé du code — en cas de divergence, le code fait foi. MySQL 8.4 · Auto-mapping Doctrine · Zéro migration au bootstrap (entités auto-mappées).
Vue d'ensemble
L'écosystème utilise deux moteurs BDD :
| Moteur | Usage | Emplacement |
|---|---|---|
| MySQL 8.4 | Données applicatives (toutes les tables ci-dessous) | 127.0.0.1:3306 |
| SQLite + sqlite-vec | Index vectoriel RAG (kNN cosinus) | /var/www/telaria/var/rag/ |
Les tables MySQL sont réparties entre 4 bundles — chacun déploie ses entités de manière autonome via auto_mapping. L'application principale (telaria-app) ne maintient que ses propres entités (user, reset_password_request, cms_content_seo).
Périmètre par bundle
telaria-app → user, reset_password_request, cms_content_seo
tlr-symfony → site, cms_content, cms_tag, cms_image, cms_content_tag (pivot)
tlr-codexia → veille_source, veille_item, veille_attempt, veille_read
app_setting, chat_config
metrics_usage, metrics_daily, metrics_alert, metrics_api_key
tlr-mcp → mcp_tenant, mcp_api_client, mcp_project, mcp_tool_audit_log
telaria-app
user
Table centrale d'authentification. Implémente Security, 2FA email/TOTP, et trusted devices.
| Colonne | Type | Nullable | Notes |
|---|---|---|---|
id |
INT AUTO_INCREMENT | non | PK |
email |
VARCHAR(255) UNIQUE | non | identifiant de connexion |
password |
VARCHAR | non | bcrypt |
roles |
JSON | non | ["ROLE_ADMIN"] — ROLE_USER ajouté dynamiquement |
is_verified |
BOOL | non | vérification e-mail initiale |
api_token |
VARCHAR(128) | oui | token opaque applicatif (≠token MCP) |
is2fa_enabled |
BOOL | non | 2FA par e-mail activé |
auth_code |
VARCHAR(255) | oui | code OTP courant (email 2FA) |
totp_secret |
VARCHAR(255) | oui | secret TOTP |
trusted_version |
INT | non | version du cookie trusted device (incrémenté à la révocation) |
Interfaces : UserInterface, TwoFactorInterface, TrustedDeviceInterface, CmsUserInterface, VeilleReaderInterface.
reset_password_request
Géré par SymfonyCasts ResetPassword. Stocke les tokens de réinitialisation hachés.
| Colonne | Type | Notes |
|---|---|---|
id |
INT AUTO_INCREMENT | PK |
user_id |
INT FK | → user.id |
selector |
VARCHAR | partie publique du token |
hashed_token |
VARCHAR | partie privée hachée |
requested_at |
DATETIME | |
expires_at |
DATETIME | TTL ~1h |
cms_content_seo
Extension SEO d'une page CMS. Pattern « companion entity » : la table cms_content appartient au bundle tlr-symfony et ne peut pas être modifiée directement.
| Colonne | Type | Nullable | Notes |
|---|---|---|---|
id |
INT AUTO_INCREMENT | non | PK |
content_id |
INT FK UNIQUE | non | OneToOne → cms_content.id CASCADE DELETE |
canonical_url |
VARCHAR(255) | oui | URL canonique custom ; null = calculée depuis la requête |
tlr-symfony
site
Un domaine servi par l'application. Résolution par host à chaque requête (SiteResolver). Porte le thème, la marque, les adresses mail propres au domaine.
| Colonne | Type | Nullable | Notes |
|---|---|---|---|
id |
INT AUTO_INCREMENT | non | PK |
host |
VARCHAR(255) UNIQUE | non | host canonique, sans schéma (ex. telaria.dev) |
aliases |
JSON | oui | hosts supplémentaires (ex. ["www.telaria.dev", "localhost"]) |
slug |
VARCHAR(64) UNIQUE | non | id stable (ex. codexia) — /^[a-z0-9-]+$/ |
label |
VARCHAR(128) | non | nom affiché |
locale_default |
VARCHAR(8) | non | défaut fr |
layout_template |
VARCHAR(128) | non | template Twig de layout — défaut cms/index.html.twig |
theme |
VARCHAR(64) | oui | clé CSS du thème |
brand_domain |
VARCHAR(64) | oui | |
brand_tld |
VARCHAR(16) | oui | |
contact_email |
VARCHAR(255) | oui | destinataire formulaire de contact |
sender_email |
VARCHAR(255) | oui | expéditeur mails sortants |
home_slug |
VARCHAR(255) | oui | slug de la page CMS racine (/) |
og_image_default |
VARCHAR(512) | oui | image Open Graph par défaut |
enabled |
BOOL | non | défaut true |
is_primary |
BOOL | non | site de repli si aucun host ne matche — un seul attendu |
created_at |
DATETIME_IMMUTABLE | non | PrePersist |
updated_at |
DATETIME_IMMUTABLE | oui | PreUpdate |
cms_content
Page ou contenu éditorial. Slug unique par site (contrainte composite (site_id, slug)).
| Colonne | Type | Nullable | Notes |
|---|---|---|---|
id |
INT AUTO_INCREMENT | non | PK |
site_id |
INT FK | non | → site.id |
author_id |
INT FK | non | → user.id (via CmsUserInterface) |
title |
VARCHAR(255) | non | |
slug |
VARCHAR(255) | non | /^[a-z0-9-]+$/ ; auto-slugifié depuis le titre à la création |
excerpt |
TEXT | oui | |
markdown |
TEXT | non | contenu source |
status |
VARCHAR(20) | non | draft | published | archived |
published_at |
DATETIME | oui | posé au premier publish() |
updated_at |
DATETIME | non | PrePersist/PreUpdate |
visibility |
VARCHAR(20) | non | public | protected | private |
type |
VARCHAR(50) | non | page (défaut) | block (fragments chrome non indexés) |
noindex |
BOOL | non | meta robots noindex + exclusion sitemap |
og_image |
VARCHAR(512) | oui | surcharge l'OG image du site |
Contrainte unique : UNIQUE(site_id, slug).
cms_tag
Étiquette transverse (vocabulaire partagé entre sites).
| Colonne | Type | Notes |
|---|---|---|
id |
INT AUTO_INCREMENT | PK |
name |
VARCHAR(255) | |
slug |
VARCHAR(255) UNIQUE | auto-slugifié depuis name |
cms_content_tag (pivot ManyToMany)
| Colonne | Type | Notes |
|---|---|---|
cms_content_id |
INT FK | → cms_content.id |
cms_tag_id |
INT FK | → cms_tag.id |
cms_image
Image média. Upload sécurisé via FileUploader (validation MIME réelle par finfo).
| Colonne | Type | Nullable | Notes |
|---|---|---|---|
id |
INT AUTO_INCREMENT | non | PK |
filename |
VARCHAR(255) | non | nom de fichier stocké |
mime |
VARCHAR(100) | non | type MIME validé |
size |
INT | non | octets |
width |
INT | oui | |
height |
INT | oui | |
checksum |
VARCHAR(64) | non | déduplication |
created_at |
DATETIME | non | |
owner_id |
INT FK | non | → user.id |
site_id |
INT FK | oui | → site.id SET NULL si site supprimé |
content_id |
INT FK | oui | → cms_content.id (rattachement optionnel) |
tlr-codexia
veille_source
Source de veille (RSS/Atom/HTML). La pipeline de collecte est pilotée par scheduler Symfony.
| Colonne | Type | Nullable | Notes |
|---|---|---|---|
id |
INT AUTO_INCREMENT | non | PK |
slug |
VARCHAR(64) UNIQUE | non | /^[a-z0-9-]+$/ |
name |
VARCHAR(128) | non | |
url |
VARCHAR(512) | non | URL du flux |
type |
VARCHAR(16) | non | rss | atom | html |
schedule |
VARCHAR(32) | non | interval ISO 8601 (ex. PT1H) |
default_theme |
VARCHAR(64) | non | thème par défaut des items — défaut ia |
is_active |
BOOL | non | interrupteur humain |
standby |
BOOL | non | pause automatique après N échecs consécutifs (distinct de is_active) |
standby_since |
DATETIME_IMMUTABLE | oui | |
standby_reason |
VARCHAR(255) | oui | |
consecutive_failures |
INT | non | compteur d'échecs consécutifs — remis à 0 au succès ou au lever de standby |
editorial_description |
TEXT | oui | |
created_at |
DATETIME_IMMUTABLE | non | |
updated_at |
DATETIME_IMMUTABLE | oui |
veille_item
Item collecté depuis une source. Journal de déduplication + cycle de vie de la proposition.
| Colonne | Type | Nullable | Notes |
|---|---|---|---|
id |
INT AUTO_INCREMENT | non | PK |
source_id |
INT FK | non | → veille_source.id CASCADE DELETE |
url |
VARCHAR(768) | non | INDEX |
content_hash |
VARCHAR(64) | non | SHA-256 du contenu canonisé (dédup si URL change) — INDEX |
title |
VARCHAR(512) | oui | |
published_at |
DATETIME_IMMUTABLE | oui | pubDate du flux |
created_at |
DATETIME_IMMUTABLE | non | moment d'ingestion — INDEX |
processed_at |
DATETIME_IMMUTABLE | oui | moment de traitement LLM — INDEX |
theme |
VARCHAR(64) | non | |
status |
VARCHAR(16) | non | pending | proposed | accepted | rejected | failed |
raw_content |
TEXT | oui | contenu brut nettoyé du HTML |
summary |
TEXT | oui | résumé Claude |
title_fr |
VARCHAR(512) | oui | titre français extrait du résumé |
relevance |
FLOAT | oui | score 0..1 (confiance classification) |
model_used |
VARCHAR(64) | oui | modèle Claude utilisé (ex. claude-haiku-4-5) |
proposal_path |
VARCHAR(512) | oui | chemin relatif du fichier proposition écrit |
error_message |
TEXT | oui | diagnostic si status=failed |
veille_attempt
Tentative de traitement d'un item. Conserve le détail complet fetch + LLM pour diagnostic.
| Colonne | Type | Nullable | Notes |
|---|---|---|---|
id |
INT AUTO_INCREMENT | non | PK |
item_id |
INT FK | non | → veille_item.id CASCADE DELETE — INDEX |
attempted_at |
DATETIME_IMMUTABLE | non | INDEX |
outcome |
VARCHAR(16) | non | success | failed |
stage_reached |
VARCHAR(16) | non | fetch | classify | summarize | write | done |
duration_ms |
INT | oui | durée totale |
fetch_attempted |
BOOL | non | |
fetch_outcome |
VARCHAR(32) | oui | |
fetch_http_status |
INT | oui | |
fetch_bytes |
INT | oui | |
fetch_extracted_chars |
INT | oui | |
fetch_error |
TEXT | oui | |
llm_model |
VARCHAR(64) | oui | |
llm_request |
TEXT | oui | prompt envoyé |
llm_response_raw |
TEXT | oui | réponse brute |
llm_http_status |
INT | oui | |
tokens_input |
INT | oui | |
tokens_output |
INT | oui | |
error_class |
VARCHAR(255) | oui | classe PHP de l'exception |
error_message |
TEXT | oui |
veille_read
État de lecture par utilisateur (absence de ligne = non lu).
| Colonne | Type | Notes |
|---|---|---|
id |
INT AUTO_INCREMENT | PK |
user_id |
INT FK | → user.id CASCADE DELETE — INDEX |
item_id |
INT FK | → veille_item.id CASCADE DELETE |
read_at |
DATETIME_IMMUTABLE |
Contrainte : UNIQUE(user_id, item_id).
app_setting
Store clé/valeur générique (singleton par clé).
| Colonne | Type | Notes |
|---|---|---|
name |
VARCHAR(128) | PK (pas d'id auto) |
value |
TEXT | nullable |
chat_config
Ligne unique (singleton) — paramétrage du chat public administré en BO.
| Colonne | Type | Notes |
|---|---|---|
id |
INT AUTO_INCREMENT | PK |
model |
VARCHAR(64) | modèle Claude (ex. claude-haiku-4-5) |
top_k |
INT | nb de documents RAG récupérés (1–20) |
score_threshold |
FLOAT | seuil de pertinence cosinus (0–1) |
temperature |
FLOAT | température LLM (0–1) |
max_tokens |
INT | limite tokens sortants (256–4096) |
history_turns |
INT | tours de conversation mémorisés (0–20) |
metrics_usage
Usage de tokens dimensionnel par (jour, modèle, clé API, service tier). Alimenté depuis l'Admin API Anthropic.
| Colonne | Type | Notes |
|---|---|---|
id |
INT AUTO_INCREMENT | PK |
day |
DATE_IMMUTABLE | INDEX |
model |
VARCHAR(128) | |
api_key_id |
VARCHAR(128) | |
service_tier |
VARCHAR(32) | |
uncached_input_tokens |
BIGINT | nullable |
cache_read_tokens |
BIGINT | nullable |
cache_creation_tokens |
BIGINT | nullable |
output_tokens |
BIGINT | nullable |
Contrainte : UNIQUE(day, model, api_key_id, service_tier).
metrics_daily
Snapshot quotidien org-wide (tokens + coûts), agrégé par label (= modèle ou type de coût).
| Colonne | Type | Notes |
|---|---|---|
id |
INT AUTO_INCREMENT | PK |
day |
DATE_IMMUTABLE | INDEX |
label |
VARCHAR(128) | modèle ou type (ex. web_search) |
uncached_input_tokens |
BIGINT | nullable |
cache_read_tokens |
BIGINT | nullable |
cache_creation_tokens |
BIGINT | nullable |
output_tokens |
BIGINT | nullable |
web_search_requests |
BIGINT | nullable |
cost_cents |
DECIMAL(14,5) | nullable — cents USD |
Contrainte : UNIQUE(day, label).
metrics_alert
Alertes de coût configurables (seuil en USD, période jour/mois, anti-spam par période).
| Colonne | Type | Nullable | Notes |
|---|---|---|---|
id |
INT AUTO_INCREMENT | non | PK |
label |
VARCHAR(128) | non | |
amount_usd |
DECIMAL(10,2) | non | seuil en dollars |
period |
VARCHAR(16) | non | daily | monthly |
email |
VARCHAR(255) | non | destinataire de l'alerte |
enabled |
BOOL | non | |
last_notified_period |
VARCHAR(16) | oui | ex. 2026-06 (anti-spam : 1 mail/franchissement/période) |
last_notified_at |
DATETIME_IMMUTABLE | oui |
metrics_api_key
Cache local des clés API de l'org Anthropic (id → nom lisible).
| Colonne | Type | Notes |
|---|---|---|
id |
VARCHAR(64) | PK (id Anthropic, pas d'auto-increment) |
name |
VARCHAR(255) | |
workspace_id |
VARCHAR(64) | nullable |
tlr-mcp
mcp_tenant
Tenant du serveur MCP (organisation cliente).
| Colonne | Type | Notes |
|---|---|---|
id |
INT AUTO_INCREMENT | PK |
name |
VARCHAR(100) UNIQUE | |
status |
VARCHAR(20) | active | ... |
mcp_api_client
Client API d'un tenant. Le token brut n'est jamais stocké — seulement son hash SHA-256.
| Colonne | Type | Nullable | Notes |
|---|---|---|---|
id |
INT AUTO_INCREMENT | non | PK |
tenant_id |
INT FK | non | → mcp_tenant.id CASCADE DELETE |
token_hash |
VARCHAR(64) UNIQUE | non | SHA-256 du token opaque |
scopes |
JSON | non | ex. ["tool:search_docs", "project:codexia"] ; wildcards tool:* / project:* supportés |
rate_limit_per_minute |
INT | non | défaut 60 |
revoked |
BOOL | non | |
expires_at |
DATETIME_IMMUTABLE | oui | null = pas d'expiration |
mcp_project
Projet RAG accessible via MCP (pointe vers un source_root sur le disque).
| Colonne | Type | Notes |
|---|---|---|
id |
INT AUTO_INCREMENT | PK |
tenant_id |
INT FK | → mcp_tenant.id CASCADE DELETE |
slug |
VARCHAR(100) | identifiant du projet |
root_path |
VARCHAR(512) | chemin absolu vers le corpus RAG |
status |
VARCHAR(20) | active | ... |
mcp_tool_audit_log
Journal d'audit des appels d'outils MCP.
| Colonne | Type | Nullable | Notes |
|---|---|---|---|
id |
INT AUTO_INCREMENT | non | PK |
tenant_id |
INT | non | dénormalisé (pas de FK — conservé si tenant supprimé) |
project_id |
INT | oui | dénormalisé |
api_client_id |
INT | oui | dénormalisé |
tool_name |
VARCHAR(100) | non | list_docs | read_doc | search_docs |
status |
VARCHAR(20) | non | success | refused | error |
error_code |
VARCHAR(50) | oui | code JSON-RPC si erreur |
timestamp |
DATETIME_IMMUTABLE | non |
Index vectoriel SQLite (RAG)
En dehors de MySQL, le moteur RAG utilise un fichier SQLite + l'extension sqlite-vec pour la recherche kNN.
| Fichier | Emplacement | Notes |
|---|---|---|
rag.sqlite |
/var/www/telaria/var/rag/ |
index vectoriel + métadonnées |
vec0.so |
/usr/local/lib/sqlite-vec/ |
extension SQLite chargée par PHP 8.5 |
Structure SQLite (gérée par tlr-rag, pas par Doctrine) :
- Table des chunks :
(id, source_path, chunk_index, content, embedding BLOB) - Recherche kNN par cosinus via
vec_search()de sqlite-vec
Patterns Doctrine notables
Auto-mapping — Toutes les entités des bundles utilisent l'auto-mapping. Zéro fichier XML/YAML de mapping. Zéro migration au bootstrap d'un bundle.
resolve_target_entities — Les bundles ne dépendent pas de App\Entity\User. Ils déclarent leurs propres interfaces (CmsUserInterface, VeilleReaderInterface) ; l'app résout l'entité cible dans doctrine.yaml. Couplage découplé = testabilité et réutilisabilité des bundles.
Lifecycle callbacks — #[ORM\HasLifecycleCallbacks] + #[PrePersist] / #[PreUpdate] pour created_at / updated_at. Pas de listeners Doctrine globaux.
Dénormalisation d'audit — mcp_tool_audit_log stocke les IDs en colonnes scalaires plutôt qu'en FK — choix délibéré pour conserver l'historique d'audit même si tenant/client est supprimé.
Singleton par PK naturelle — app_setting utilise name comme PK (VARCHAR 128, pas d'auto-increment). metrics_api_key idem avec l'ID Anthropic.
Voir aussi
architecture-symfony.md— vue d'ensemble bundles, patterns Doctrine, authtutos/ia/rag-bout-en-bout.md— pipeline RAG complet (ingest → recherche)tutos/ia/serveur-mcp-symfony.md— bundle MCP, scopes, quotas