Application de listes et kanban collaboratives en temps réel, construite sur Elixir/Phoenix/Ash : synchronisation live, rôles et invitations, verrouillage optimiste, ordre LexoRank et conformité RGPD.
Listify est une application de listes et tableaux kanban collaboratifs en temps réel : plusieurs personnes éditent les mêmes listes, voient les changements apparaître instantanément, et gèrent membres, rôles et notifications. Un projet greenfield piloté par une spécification détaillée, en production et auto-hébergé.

Je voulais un terrain réel pour pousser la stack Elixir/Phoenix/Ash sur un domaine non trivial : la collaboration temps réel. Pas un todo-list de démo, mais une application complète — concurrence d’édition, rôles, invitations, notifications, audit, RGPD — pilotée par une spec écrite avant le code, pour verrouiller les décisions d’architecture en amont plutôt que de les improviser.
L’application est 100 % Phoenix LiveView — aucun framework SPA, l’interactivité est pilotée par le serveur via WebSocket. La logique métier, la persistance et l’autorisation passent par Ash 3.4 (AshPostgres), organisée en domaines : Accounts, Lists, Messaging, Notifications, Audit, Backups, Moderation.
Phoenix LiveView (UI server-driven)
│
├── Ash domains (logique, autorisation, persistance)
│ Accounts · Lists · Messaging · Notifications · Audit · Backups
├── Phoenix PubSub (signalisation temps réel)
├── Oban (jobs : mailer, exports, scheduled, critical)
└── PostgreSQL 16 + Redis (sessions)
Chaque mutation d’item ou de liste publie un message sur un topic PubSub (items:list:<id>). Mais le point clé : la LiveView ne consomme pas le payload diffusé — elle reçoit un simple signal ({:item_updated, id}) et relit la ressource complète via Ash, autorisations et policies appliquées. Le PubSub sert de mécanisme de signalisation, pas de transport de données — ce qui élimine la classe entière des bugs de « payload périmé ».
L’édition concurrente d’un item est protégée par un lock_version vérifié atomiquement dans la clause SQL WHERE (pas après lecture — donc pas de race). Un conflit renvoie un 409 avec la version serveur pour que le client recharge. Mais seuls les champs « lourds » (titre, description, labels) sont protégés ; les champs chauds (statut, rang, assignation) passent par des actions last-write-wins séparées pour rester rapides. Ce n’est pas du verrouillage pessimiste — c’est de la détection de conflit ciblée.

Le classement des items utilise LexoRank (chaînes de rang) plutôt que des positions entières : réordonner insère un rang entre deux voisins en O(1), sans jamais renuméroter ni verrouiller une séquence globale. Deux réordonnancements concurrents ne se coordonnent pas — la math des chaînes garantit la monotonie.
Topologie de files fixée dans la spec : default (10), mailer (5), exports (3), scheduled (2), critical (1). Une quinzaine de workers : mails d’invitation et de mention, rappels d’échéance, digest quotidien, réinitialisation des listes récurrentes, export RGPD, purge de compte différée, nettoyage des sauvegardes S3.
listify.josephpire.dev (Docker Swarm, 2 répliques, rolling updates)Projets similaires