Statut : brouillon v1 — à valider avant implémentation Dernière mise à jour : 2026-08-08
Application web permettant de créer et réviser des cartes de deux natures :
- Association — un indice (ex. un code) à associer à une réponse, révisé dans le désordre.
- Récitation — un texte long révisé phrase par phrase, dans l'ordre.
Le système est collaboratif : plusieurs utilisateurs peuvent co-éditer un même deck. La progression d'apprentissage reste strictement individuelle.
Backend : Supabase (Postgres + Auth + Row Level Security + API auto-générée).
| Sujet | Décision |
|---|---|
| Unification des types de cartes | Une seule entité card — plus de distinction structurelle deck/carte, tout vit dans une seule table |
| Front/back | Non séparés en colonnes — stockés dans un seul champ texte brut, interprété à la volée (plus de JSON) |
| Format de saisie | Syntaxe markdown-pipe : indice|réponse par ligne pour l'association ; une phrase par ligne, sans pipe, pour la récitation |
| Sous-unité (paire/ligne) | Pas d'id stocké — identifiée par le hash de son propre texte au moment de la lecture (voir §3.3) |
order explicite |
Absent — l'ordre est la position de la ligne dans le texte |
| Statut d'apprentissage | Propriété de l'utilisateur, au niveau de la card entière — pas de granularité par ligne |
| Collaboration | Édition ouverte à tout utilisateur authentifié — pas de liste de collaborateurs, pas d'invitation |
| Échelle de la communauté | Ouverte, taille inconnue |
| Gestion des conflits d'édition | Dernier écrit gagne, au niveau de la card entière — risque accepté, voir §7 |
| Backend | Supabase (Postgres, Auth, RLS) |
| Modération / suppression | Suppression douce par statut (normal / signalee / deleted) plutôt qu'un delete SQL — voir §4.2 |
| Rôles utilisateur | Trois rôles (membre, manager, admin), stockés dans user_roles, gérés uniquement en base — jamais via l'application front |
| Authentification | Email + mot de passe et lien magique, au choix de l'utilisateur |
- Une seule table,
cards. Ce que la v1 de cette spec appelaitdeck— le titre, le type, la langue, la difficulté, et le contenu entier — vit maintenant dans une seule ligne de cette table. - Le contenu est un champ texte brut, jamais du JSON. Le texte est interprété (parsé) à la volée à chaque lecture, côté client, avec la syntaxe pipe/ligne définie en §3.3.
- Il n'y a pas d'
idstocké pour chaque paire/ligne à l'intérieur du texte, et pas de statut par ligne non plus. Le statut d'apprentissage s'applique à la card entière : "en cours" ou "terminé" porte sur l'ensemble du contenu, pas sur une paire/ligne précise. - La progression reste une table séparée, une ligne par (utilisateur, card), jamais mutable par personne d'autre que son propriétaire.
- Édition ouverte : n'importe quel utilisateur authentifié peut modifier n'importe quelle
card. Pas de notion de collaborateurs.
create extension if not exists "pgcrypto";
-- ---------------------------------------------------------------
create table cards (
id uuid primary key default gen_random_uuid(),
title text not null,
type text not null check (type in ('association', 'recitation')),
lang text not null default 'fr', -- ISO 639-1
content text not null default '', -- texte brut, voir §3.3 pour la syntaxe
visibility text not null default 'public' check (visibility in ('public', 'unlisted', 'private')),
status text not null default 'normal' check (status in ('normal', 'signalee', 'deleted')),
owner_id uuid not null references auth.users(id),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
updated_by uuid references auth.users(id)
);
-- ---------------------------------------------------------------
-- Rôles utilisateur — gérés exclusivement en base, jamais depuis le front.
create table user_roles (
user_id uuid primary key references auth.users(id) on delete cascade,
role text not null default 'membre' check (role in ('membre', 'manager', 'admin')),
created_at timestamptz not null default now()
);
-- ---------------------------------------------------------------
-- Progression individuelle — jamais partagée entre utilisateurs.
-- Statut au niveau de la card entière, pas de granularité par ligne.
create table progress (
user_id uuid not null references auth.users(id) on delete cascade,
card_id uuid not null references cards(id) on delete cascade,
status text not null default 'en_cours' check (status in ('en_cours', 'termine')),
last_reviewed_at timestamptz,
review_count integer not null default 0,
primary key (user_id, card_id)
);
-- ---------------------------------------------------------------
-- Historique de versions du texte — filet de sécurité anti-vandalisme,
-- indispensable puisque l'édition est ouverte à tous sans validation préalable (voir §7)
create table card_revisions (
id bigint generated always as identity primary key,
card_id uuid not null references cards(id) on delete cascade,
content text not null,
edited_by uuid references auth.users(id),
edited_at timestamptz not null default now()
);type = 'association'
13|Bouches-du-Rhône
06|Alpes-Maritimes
Règle de parsing : chaque ligne non vide contenant un | produit une paire {front, back}. Tout ce qui précède le premier | est front, tout ce qui suit est back.
type = 'recitation'
Maître Corbeau, sur un arbre perché,
Tenait en son bec un fromage.
Règle de parsing : chaque ligne non vide sans | produit un item {text}, dans l'ordre d'apparition — cet ordre porte l'information de séquence, il n'y a rien d'autre à stocker pour ça.
Ordre (pour recitation uniquement) : la position de la ligne dans le texte porte l'information de séquence — rien de plus à stocker pour ça.
langreste porté par lacardentière (toutes les lignes qu'elle contient partagent la même langue) — hypothèse non re-questionnée depuis §8 v1, à confirmer.- La notion de difficulté (
difficulty) a été retirée du schéma : jugée non utile, elle n'apportait pas de valeur distincte de la difficulté perçue individuellement par chaque utilisateur.
- Auth via Supabase Auth : email + mot de passe, ou lien magique — les deux options sont proposées côté client (
Login.tsx), le provider "email" de Supabase gère les deux sans distinction en base. cards.visibility = 'public'→ lecture libre par tout le monde (y compris non connecté).- Édition du
content→ ouverte à tout utilisateur authentifié, quel que soit le propriétaire. Pas de liste de collaborateurs à gérer. progress→ RLS stricte : un utilisateur ne peut lire/écrire que ses propres lignes (user_id = auth.uid()).
alter table cards enable row level security;
alter table progress enable row level security;
alter table card_revisions enable row level security;
alter table user_roles enable row level security;
-- Lecture publique des cards publiques ; une card 'deleted' reste masquée
-- sauf pour son propriétaire ou un manager/admin
create policy "cards_read_public" on cards
for select using (
(visibility = 'public' or owner_id = auth.uid())
and (status <> 'deleted' or owner_id = auth.uid() or public.current_user_role() in ('manager', 'admin'))
);
-- Édition ouverte à tout utilisateur authentifié — le contrôle des
-- transitions de `status` se fait via trigger, voir §4.2
create policy "cards_update_open" on cards
for update using (auth.uid() is not null);
-- Création : tout utilisateur authentifié devient owner_id de sa propre
-- card, qui démarre toujours en statut 'normal'
create policy "cards_insert_authenticated" on cards
for insert with check (auth.uid() = owner_id and status = 'normal');
-- Progression strictement individuelle
create policy "progress_owner_only" on progress
for all using (user_id = auth.uid());
-- Historique lisible par tous, écrit uniquement via trigger (voir §5)
create policy "revisions_read_public" on card_revisions
for select using (true);
-- Rôle lisible par soi-même, ou par un manager/admin ; jamais d'écriture
-- exposée au client (voir §4.2)
create policy "user_roles_read_self_or_privileged" on user_roles
for select using (
user_id = auth.uid()
or exists (select 1 from user_roles ur where ur.user_id = auth.uid() and ur.role in ('manager', 'admin'))
);- Pas de
deleteSQL surcards: la suppression est un changement destatus, réversible. - Trois rôles, stockés dans
user_roles(un rôle par utilisateur,membrepar défaut à la création du compte) :- membre : peut créer une card et la faire passer en statut
signalee(signalement en cas de problème). Ne peut pas la faire passer endeletedni la restaurer ennormal. - manager / admin : peuvent faire passer une card en
deleted(suppression douce) ou la remettre ennormal(restauration).
- membre : peut créer une card et la faire passer en statut
- Contrôle appliqué par un trigger Postgres (
enforce_card_status_transition,before update on cards) qui vérifie le rôle de l'auteur viauser_roles— la logique de droits vit entièrement en base, jamais dans le front. Le front affiche les actions à tout utilisateur authentifié et se contente de relayer l'erreur renvoyée par la base si l'action est refusée. - Une card
deletedest masquée de la lecture publique (cards_read_public), sauf pour son propriétaire ou un manager/admin (pour pouvoir la consulter et éventuellement la restaurer). - L'attribution des rôles (
membre→manager/admin) se fait par écriture directe dansuser_rolesen base (SQL) — aucune policy d'écriture n'est exposée au client sur cette table.
- Créer une card : titre, type, langue, difficulté → contenu texte vide.
- Éditer le contenu : zone de texte brut (syntaxe pipe/ligne) → écriture de
contentcomplet en une seule requêteupdate, ouverte à tout utilisateur authentifié. Un trigger côté DB archive l'ancienne valeur danscard_revisionsavant chaqueupdate(voir §4.1 — insertion automatique, pas de policy d'écriture manuelle nécessaire). - Réviser (association) : parsing du
contentà la lecture → toutes les paires de la card sont mises en jeu à chaque session (le statut, étant global à la card, ne filtre pas les paires individuelles). En fin de session, l'utilisateur marque la card entièretermineouen_cours. - Réciter (récitation) : parsing du
contentà la lecture → parcours séquentiel complet des lignes dans l'ordre du texte, du début à la fin. En fin de parcours, l'utilisateur marque la card entièretermineouen_cours. - Consulter l'historique d'une card : lecture de
card_revisions, restauration possible d'une version antérieure par n'importe quel utilisateur authentifié (cohérent avec l'édition ouverte — pas de restriction au seul propriétaire). - Signaler / supprimer / restaurer une card : changement de
statusviaupdate, ouvert à tout utilisateur authentifié côté front — le triggerenforce_card_status_transitionaccepte ou rejette selon le rôle de l'auteur (voir §4.2).
- Frontend : React (SPA), client
@supabase/supabase-js. - Backend : Supabase (Postgres 15+, Auth, RLS, API REST auto-générée via PostgREST).
- Pas de couche serveur custom nécessaire pour la V1 — toute la logique métier passe par des policies RLS + requêtes côté client.
- Écrasement en cas d'édition simultanée de la même card : une écriture remplace l'intégralité du
content. Deux contributeurs modifiant la même card en parallèle → le second écrase le premier. Mitigation partielle :card_revisionspermet un rollback manuel, mais ne prévient pas la perte initiale. À surveiller si l'usage réel montre des collisions fréquentes. - Édition totalement ouverte, sans validation préalable : conséquence directe de l'abandon de
deck_collaborators— n'importe quel utilisateur authentifié peut modifier n'importe quelle card, y compris celles qu'il n'a pas créées. C'est un choix assumé (modèle wiki), mais ça expose à du vandalisme ou des modifications de mauvaise foi dès le premier jour.card_revisionsest le seul garde-fou (rollback a posteriori, jamais de blocage a priori). - Pas de filtrage par item lors d'une session de révision : le statut étant global à la card, une session de révision remet systématiquement en jeu la totalité des paires — y compris celles que l'utilisateur maîtrise déjà. Sur une card à 100 paires, ça veut dire réviser les 100 à chaque fois, sans notion de "ce qu'il me reste à apprendre". Accepté comme conséquence directe du refus de statut par ligne.
- Pas de co-édition temps réel : deux personnes ouvrant le même éditeur au même moment ne se voient pas mutuellement. Hors périmètre V1.
- Modération : signalement (
signalee) et suppression douce (deleted) par statut, contrôlés par rôle (§4.2). Pas encore de mécanisme de blocage d'utilisateur ni de file de modération dédiée (liste des cards signalées) — à considérer si le volume de signalements le justifie.
- Portée de
lang: au niveau de lacardentière (hypothèse actuelle du schéma) ou par ligne/paire individuelle (nécessiterait de la glisser dans le format pipe, ex.13|Bouches-du-Rhône|fr) ? - Granularité du statut : confirmé au niveau card entière suite à la dernière décision — si une liste de 100 paires doit un jour permettre de cibler ce qui reste à apprendre, il faudra revenir sur ce point (fractionner la card en plusieurs cards plus petites est le contournement le plus simple avec le modèle actuel, plutôt que réintroduire un statut par ligne).
- Trigger d'archivage :
card_revisionss'alimente via un trigger Postgresbefore update on cards(à écrire) plutôt que par une écriture explicite côté client, pour garantir qu'aucune édition n'échappe à l'historique.
- Co-édition temps réel (CRDT / WebSockets).
- Répétition espacée algorithmique (type SM-2/FSRS) — le statut reste binaire
en_cours/termine, géré manuellement par l'utilisateur. - Classements ou comparaison sociale des scores entre utilisateurs.
- Export/import multi-format (CSV, Anki
.apkg, etc.).