Excalidraw Multi

Un Excalidraw dont le serveur est la source de vérité : chaque canvas vit dans le cloud (ou sur votre machine), se partage par un lien, et s'ouvre aussi bien à des humains qu'à des agents.

Proposition d'architecture, 23 octobre 2026. Travail de recherche : code source d'Excalidraw (upstream b7de45e, package 0.18), excalidraw-room, tldraw sync, Nextcloud Whiteboard, Better Auth, spec MCP. Rien n'est implémenté.

Je crée un canvas→je le partage→j'envoie un lien magique

Ce qu'Excalidraw fait aujourd'hui, et ce qu'on retourne

Excalidraw est browser-first : la scène vit dans l'onglet (localStorage), la collaboration passe par un relais aveugle (excalidraw-room, 150 lignes, chiffré de bout en bout avec la clé dans l'URL), et la persistance Firebase ne stocke que du chiffré. Le serveur ne peut donc ni lire un canvas, ni le lister, ni l'ouvrir à un agent. Nous inversons ça : le serveur possède le canvas, le navigateur en est une vue.

Aujourd'hui (excalidraw.com) Navigateur Ascène + localStorage Navigateur Bscène + localStorage excalidraw-roomrelais, ne lit rien Firebaseblobs chiffrés tout est chiffré, clé jamais côté serveur clé AES dans l'URL#room=id,clé Demain (Excalidraw Multi) Navigateur Aune vue Navigateur Bune vue AgentREST / MCP Serveurpossède le canvasauth, droits, historiquetemps réel, fichierslisible par les agents Postgres + fichiers
À gauche, le serveur est un tuyau. À droite, c'est la source de vérité : c'est ce qui rend possibles les droits, l'historique, le SSO et les agents.

On garde

  • L'éditeur @excalidraw/excalidraw tel quel (package npm, MIT), aucune modification du moteur de rendu.
  • Le modèle d'élément : version, versionNonce, index fractionnaire pour le z-order, tombstones isDeleted.
  • La règle de réconciliation d'Excalidraw (déterministe, éprouvée sur excalidraw.com).
  • Le format .excalidraw : import/export 100 % compatibles.

On jette

  • excalidraw-room et Firebase : remplacés par un seul serveur qui persiste et arbitre.
  • Le chiffrement de bout en bout par défaut : incompatible avec un serveur qui lit, liste, historise et sert des agents.
  • Le double modèle « scène locale » / « salle » : un canvas a toujours une identité serveur.
  • La resynchronisation complète toutes les 20 s : remplacée par un numéro de séquence.

Architecture cible : un seul process, une base, un dossier

Un process Node/TypeScript sert tout : l'application web, l'API, les WebSockets temps réel et l'authentification. À côté, Postgres et un dossier de fichiers. Rien d'autre n'est obligatoire. C'est volontairement ennuyeux : c'est ce qui rend l'auto-hébergement trivial.

NavigateursReact + éditeur Excalidraw Agents IAclient MCP, scripts Fournisseur SSOOIDC, optionnel SMTPliens magiques Caddy (TLS) Un process Node (Hono) Auth : Better Authmagic link, code OTP, invités, OIDC/SSO, clés API API HTTP + MCPcanvas, partage, fichiers, éléments (agents) Rooms temps réel (WebSocket)1 room en mémoire par canvas ouvert, arbitre les éditions Fichiers statiques + écriture différéesert le front, flush des rooms vers la base Postgresusers, canvas, éléments, droits Fichiersdisque local, ou S3 HTTPS + WebSocket docker compose up = app + postgres (+ mailpit en dev, minio si vous voulez du S3) Pas de Redis, pas de file de messages, pas de second service d'identité. Une instance = un process. Au-delà de quelques milliers de canvas simultanés on shardera par id de canvas, pas avant. Variables d'env : BASE_URL, DATABASE_URL, AUTH_SECRET, SMTP_URL, FILES_DIR (ou S3_*), et facultativement OIDC_ISSUER / OIDC_CLIENT_ID / OIDC_CLIENT_SECRET.
Les trois briques vertes partagent le même process et la même base : l'auth sait qui est connecté au WebSocket, l'API agents passe par la même room que les humains.

Stratégie de fork

On ne forke pas le monorepo Excalidraw. On crée un nouveau dépôt qui consomme le package npm @excalidraw/excalidraw et réécrit seulement ce que l'app excalidraw.com fait autour : la couche collab (Collab.tsx, Portal.tsx), la persistance (firebase.ts), le partage. C'est environ 2 000 lignes à remplacer, et on suit les versions upstream par un simple bump de dépendance. L'éditeur expose tout ce qu'il faut : onChange, updateScene, addFiles, onPointerUpdate, collaborateurs, mode suivi.

Sync temps réel : le serveur arbitre, en 4 messages

Pas de CRDT. Le serveur garde chaque canvas ouvert en mémoire, applique la règle de réconciliation d'Excalidraw (l'élément de version la plus haute gagne, à égalité le nonce le plus bas), numérote chaque changement accepté, et le diffuse. Le client applique les patchs reçus sans les mettre dans son undo, exactement comme aujourd'hui. L'undo multijoueur d'Excalidraw continue de fonctionner tel quel.

AliceServeur (room du canvas)Bob push { éléments modifiés (objets complets) } réconcilie version/nonceseq = 1042, dirty += ids patch { seq 1042, éléments } ack { seq 1042 } (ou les versions serveur si Alice était en retard) présence { curseur, sélection } ~30 Hz, jamais stocké flush ≤ 1 s → Postgres hello { canvas, lastSeq 1030 } (reconnexion) welcome { éléments dont seq > 1030, membres }
Quatre messages durables (hello, welcome, push, patch) plus une voie volatile pour la présence. Le numéro de séquence remplace le « tout renvoyer toutes les 20 secondes ».

Pourquoi ce choix

Transport

WebSocket brut (bibliothèque ws), messages JSON, taille max 10 Mo, heartbeat. On abandonne socket.io : il impose des sessions collantes et un adaptateur Redis dès qu'on dépasse un process, pour un protocole qu'on réécrit de toute façon. Les fichiers (images) ne passent jamais par le socket : seulement leurs identifiants, le binaire voyage en HTTP avec un cache immuable.

Identité et partage : le lien d'abord, le compte ensuite

Trois principes. Un lien suffit pour éditer : celui qui l'ouvre devient un invité anonyme avec un prénom et une couleur, sans rien saisir. Le compte, c'est un email : lien magique + code à 6 chiffres, jamais de mot de passe. Le SSO se branche, il ne se construit pas : on est client OIDC, pas fournisseur.

1. Email saisiPOST /sign-in/magic-linklimite 5 / min 2. « Vérifiez vos mails »+ champ code à 6 chiffres(si le lien est ouvert ailleurs) 3. Clic sur le lien/auth/confirm#t=jetonGET sans effet de bord 4. Bouton « Se connecter »POST vérifie le jeton5 min, usage unique, haché Scanner d'emailsSafe Links, Proofpoint… il suit le GET, voit une page inerte,ne voit pas le fragment #t=, ne clique pas :le jeton survit. Ensuite : « Ajouter une passkey pour sauter l'email la prochaine fois ». Sans SMTP configuré : le lien s'affiche dans les logs et la console admin, uniquement hors production. Bibliothèque : Better Auth (MIT, dans le process, Postgres). Plugins magicLink, emailOTP, anonymous, passkey, genericOAuth/SSO, apiKey, mcp. Auth.js l'a rejoint en 2025, Lucia est abandonné.
Le piège classique des liens magiques, ce sont les scanners d'entreprise qui « cliquent » avant l'utilisateur. Le jeton dans le fragment d'URL plus un bouton explicite règlent le problème.

Modèle de droits, minimal mais complet

QuiComment il arriveCe qu'il peut faire
PropriétaireA créé le canvasTout, y compris supprimer, gérer les liens et les membres
Membre (éditeur / lecteur)Invité par email, ou invité anonyme promu après connexionSelon son rôle ; survit à la révocation des liens
Porteur de lienOuvre /s/<jeton> ; le jeton est échangé une fois et l'URL redevient propreÉdition ou lecture selon le lien ; plusieurs liens par canvas, chacun révocable, avec expiration optionnelle
PublicCanvas marqué « lecture publique »Lecture seule, sans présence
AgentClé API de son propriétaire, ou OAuth 2.1 via MCPLes mêmes droits que la personne pour qui il agit

Résolution : rôle de membre, sinon lien valide, sinon réglage par défaut du canvas, sinon refus. Le bouton « Partager » crée un lien d'édition sans expiration et le copie : c'est tout le parcours nominal. Les organisations (plugin Better Auth, rôles owner/admin/member) arrivent en phase 3, c'est aussi là que le SSO par domaine d'email prend son sens.

Données : six tables, pas de magie

TableRôleColonnes clés
users, sessions, accounts…Gérées par Better Authis_anonymous pour les invités, fusion à la connexion
canvasesUn canvasid, owner_id, title, link_access, public_read, seq, app_state (fond, grille)
elementsÉtat courant, un élément par ligne(canvas_id, element_id), version, version_nonce, is_deleted, seq, data jsonb
element_logHistorique (chaque version acceptée)canvas_id, seq, element_id, actor_id, data ; purgeable
canvas_members, share_links, link_grantsDroitsrôle, token_hash, expires_at, revoked_at
files, canvas_filesImages, adressées par contenusha256, mime, size, chemin disque ou clé S3 ; GC des orphelins

Chargement d'un canvas = select … where canvas_id = ? and seq > ?. Écriture = upsert par lot depuis la room, toutes les secondes au plus. Un crash du process perd au pire une seconde d'édition. L'export .excalidraw est une simple projection de elements.

Agents : des collaborateurs comme les autres (phase 2)

Parce que le serveur lit la scène, un agent n'a besoin de rien de spécial : il passe par la même room qu'un humain. On l'expose de deux façons, calquées sur ce que font Miro et Figma en 2026.

Point d'attention technique : le rendu PNG côté serveur demande un DOM (les utilitaires d'export d'Excalidraw s'appuient sur canvas/SVG du navigateur). Solution simple : un Chromium headless dans l'image Docker, ou demander la capture à un client connecté. À trancher en phase 2.

Stack et déploiement

BriqueChoixPourquoi celui-là
FrontVite + React + @excalidraw/excalidrawMême outillage qu'upstream, l'éditeur reste un package
ServeurNode 22+, TypeScript, Hono, wsHono parle Request/Response standard, Better Auth s'y branche en une ligne ; un seul process
AuthBetter AuthSeule option MIT, dans le process, avec magic link + OTP + invités + OIDC/SSO + clés API + MCP
BasePostgres 16 via DrizzleComptes, droits, listing, recherche et historique au même endroit ; jsonb pour les éléments
FichiersDisque local par défaut, driver S3 optionnelZéro dépendance pour l'auto-hébergeur, MinIO/R2 pour les autres
EmailSMTP (une URL), Mailpit en devCouvre Resend, Postmark, Gmail, relais d'entreprise
DéploiementUne image Docker, un docker-compose.yml, Caddy devantLe même que celui qu'on utilise sur ce VPS
services:
  app:       # front + API + WS + auth, port 3000
    image: excalidraw-multi
    environment: [BASE_URL, DATABASE_URL, AUTH_SECRET, SMTP_URL, FILES_DIR=/data/files]
    volumes: [files:/data/files]
  postgres:
    image: postgres:16
    volumes: [pg:/var/lib/postgresql/data]

Quatre phases, chacune utilisable

1. Le cœur
  • Serveur + Postgres + room
  • Créer, ouvrir, éditer à plusieurs
  • Lien de partage édition
  • Invités anonymes
  • Images
  • Compose auto-hébergeable
2. Comptes & agents
  • Magic link + code
  • « Mes canvas »
  • Liens lecture, révocation
  • API éléments + MCP
  • Clés API
3. Équipes
  • Organisations
  • OIDC/SSO par domaine
  • Dossiers, recherche
  • Historique et restauration
  • Passkeys
4. Si besoin
  • Deltas par propriété
  • Offline long
  • Sharding des rooms
  • Commentaires, export serveur

La phase 1 est celle qui valide le pari technique : une room serveur qui tient la charge d'une session à dix personnes, avec reconnexion propre. Tout le reste est du produit.

Ce qui mérite votre décision

Pour chaque point : ma recommandation, et l'alternative crédible. Le reste du document suppose la recommandation.

1

Chiffrement de bout en bout

Le garder rend le serveur aveugle : pas d'agents, pas de recherche, pas d'historique lisible, pas de miniatures.

Reco : l'abandonner. Chiffrement au repos côté serveur, TLS en transit.

Alternative : un mode « canvas privé E2E » opt-in, sans agents ni historique. Coût : deux chemins de code à maintenir.

2

Modèle de sync

Élément entier (règle Excalidraw) ou delta par propriété (StoreDelta, style tldraw) ?

Reco : élément entier en v1, protocole conçu pour accueillir les deltas plus tard.

Alternative : deltas dès le départ. Meilleure fidélité en édition simultanée du même objet, mais c'est le chantier qu'Excalidraw a laissé inachevé en open source : risque réel.

3

Base de données

Postgres ou SQLite en binaire unique ?

Reco : Postgres. Un conteneur de plus, mais multi-process possible, sauvegardes connues, jsonb.

Alternative : SQLite + Litestream pour un « un seul binaire ». Séduisant pour l'auto-hébergeur solo, mais on s'interdit tout second process pour toujours.

4

Fork ou package

Forker le monorepo Excalidraw ou consommer le package npm ?

Reco : le package npm + réécriture de la coque applicative.

Alternative : fork du monorepo. Permet de patcher l'éditeur, mais chaque mise à jour upstream devient un rebase douloureux.

5

Qui peut créer un canvas

N'importe qui avec un email (instance ouverte) ou seulement les comptes invités par un admin ?

Reco : les deux, par variable d'env, ouvert par défaut. Les invités via lien éditent toujours sans compte.

Décision produit plus que technique, mais elle fixe la page d'accueil et le premier écran.

6

Invités anonymes

Un lien d'édition ouvert par un inconnu : il édite tout de suite, ou il doit donner un email ?

Reco : il édite tout de suite, avec un prénom et une couleur, comme Excalidraw et tldraw.com. Le propriétaire peut exiger un compte par canvas.

Alternative : email obligatoire. Plus traçable, mais casse le « j'envoie un lien, c'est fini ».

7

Nom et marque

La licence est MIT et il n'y a pas de clause de marque, mais « Excalidraw » reste leur nom.

Reco : un nom propre dès le premier commit, « propulsé par Excalidraw » dans le pied de page.

Garder « Excalidraw Multi » comme nom de code interne seulement.

8

Taille des images

Excalidraw limite à 4 Mo parce que les images voyagent en base64 dans la scène. Chez nous elles voyagent en HTTP.

Reco : 10 Mo par défaut, configurable.

Garder 4 Mo pour rester identique à l'export upstream.

Risques et ce qu'on fait avec

Better Auth bouge vite (changements entre mineures). On fige les versions et on l'enveloppe derrière un module auth/ à nous, seul endroit qui le connaît.
Les éditions simultanées du même élément perdent une des deux. Connu, accepté par excalidraw.com depuis 2020, et le protocole prévoit l'upgrade vers les deltas (décision 2).
Un process = un point de panne. Perte max d'une seconde d'édition au crash grâce au journal ; redémarrage en quelques secondes ; les clients repoussent leurs modifications non acquittées.
Le rendu serveur (miniatures, export PNG pour agents) demande un navigateur headless. Hors phase 1, tranché en phase 2.
Upstream peut sortir sa propre sync (le travail Excalidraw+ existe en privé). Si ça arrive en open source, on consomme le package npm, donc on pourra l'adopter sans jeter notre serveur de droits et de persistance.

Notes de recherche détaillées (sources, numéros de ligne, URL) : ~/excalidraw-multi/research/. Références clonées : ~/excalidraw-multi/ref/.