●  4 usuarios online WebSocket WSS Offline-First MongoDB Atlas
🏗️ Arquitectura de Sincronización — NoteFlow CRDT Stack
Offline-First
Ana García (PM)
Chrome / MacBook
Y.Doc Tiptap IndexedDB
Carlos Ruiz (Dev)
Firefox / Linux
Y.Doc Tiptap IndexedDB
Laura Sánchez (UX)
Safari / iPhone — OFFLINE
Y.Doc Tiptap IndexedDB ✓
Marcos Torres (CTO)
Edge / Windows
Y.Doc Tiptap IndexedDB
WSS :443
y-websocket
Broadcast
CRDT updates
y-websocket Server
Node.js · Puerto 443 (WSS)
setupWSConnection + auth JWT
setPersistence → MongoDB Atlas
flushSize: 100 updates / batch
Awareness: cursores + presencia
💻 Implementación — Archivos Clave de NoteFlow
TypeScript · React
CollaborativeEditor.tsx
provider.ts
yjs-server.ts
offline.ts
// src/components/CollaborativeEditor.tsx — NoteFlow editor con presencia
import { useEditor, EditorContent } from "@tiptap/react";
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
import { IndexeddbPersistence } from "y-indexeddb";

const NOTEFLOW_USERS = {
  ana:    { name: "Ana García",     color: "#4F46E5" },
  carlos: { name: "Carlos Ruiz",    color: "#0EA5E9" },
  laura:  { name: "Laura Sánchez", color: "#10B981" },
  marcos: { name: "Marcos Torres", color: "#F59E0B" },
};

export function CollaborativeEditor({ documentId, userId }: Props) {
  // 1. Documento CRDT compartido entre todos los clientes
  const doc = useMemo(() => new Y.Doc(), []);

  // 2. Persistencia offline en IndexedDB (1 línea)
  const _ = useMemo(() =>
    new IndexeddbPersistence(`noteflow-${documentId}`, doc), [doc, documentId]);

  // 3. WebSocket provider (solo WSS :443, sin WebRTC)
  const provider = useMemo(() =>
    new WebsocketProvider(
      "wss://collab.noteflow.app",
      documentId,
      doc,
      { params: { token: getAuthToken() } }
    ), [doc, documentId]);

  // 4. Awareness: nombre + color del cursor visible para otros
  useEffect(() => {
    provider.awareness.setLocalStateField("user", NOTEFLOW_USERS[userId]);
  }, [provider, userId]);

  // 5. Editor Tiptap con CRDT (sin history nativo → Yjs lo gestiona)
  const editor = useEditor({
    extensions: [
      StarterKit.configure({ history: false }),   // ← CRÍTICO
      Collaboration.configure({ document: doc }),
      CollaborationCursor.configure({ provider,
        user: NOTEFLOW_USERS[userId] }),
    ],
  });

  // Cleanup al desmontar
  useEffect(() => () => {
    provider.destroy(); doc.destroy();
  }, [doc, provider]);

  return (
    <div className="editor-wrapper">
      <PresenceBar provider={provider} />
      <EditorContent editor={editor} />
      <ConnectionBadge provider={provider} />
    </div>
  );
}
Resolución Automática de Conflictos — Simulación CRDT
Sin pérdida de datos
A
t=0ms — Estado inicial (todos sincronizados)
Todos los clientes tienen el mismo documento. Ana y Carlos empiezan a editar en paralelo sin conexión entre sí (Laura se ha desconectado).
"El objetivo de NoteFlow es simplificar la colaboración."
C
t=120ms — Carlos inserta "equipos distribuidos" (posición 42)
Carlos añade texto en el centro de la frase. Su Y.Doc local registra la operación con vector clock único.
"El objetivo de NoteFlow es simplificar la colaboración de equipos distribuidos."
A
t=185ms — Ana inserta "en tiempo real" (posición 47, concurrent)
Ana, sin recibir aún el update de Carlos, edita la misma zona. CRDT asigna un identificador de posición único (lamport timestamp + clientID).
"El objetivo de NoteFlow es simplificar la colaboración en tiempo real."
t=210ms — Servidor aplica ambas operaciones (merge determinista)
Y.applyUpdate() fusiona los dos vectores. Como las inserciones tienen posiciones diferentes (identificadores únicos), ambas se preservan. Orden determinado por clientID: Ana (4f46) < Carlos (0ea5).
"El objetivo de NoteFlow es simplificar la colaboración de equipos distribuidos en tiempo real."
L
t=8min — Laura reconecta (había editado offline)
Laura añadió "de forma asíncrona" mientras estaba offline. IndexedDB tenía su estado local. Al reconectar, y-websocket sincroniza automáticamente su Y.Doc con el servidor. El merge CRDT preserva todos los cambios sin intervención manual.
"El objetivo de NoteFlow es simplificar la colaboración de equipos distribuidos en tiempo real de forma asíncrona."
Resultado: 0 conflictos, 0 pérdidas de datos. Los 3 usuarios ven exactamente el mismo documento final. Laura no tuvo que "resolver conflictos" manualmente. Esto es lo que reemplaza el 23% de tickets de soporte de NoteFlow.
👥 Usuarios Online Ahora
4 / 4
AG
Ana García
Editando §3 — "Objetivos del Q3"
CR
Carlos Ruiz
Seleccionando §1 · cursor visible
LS
Laura Sánchez
Offline — última vez hace 8 min
MT
Marcos Torres
Leyendo (sin cursor activo)
📊 Impacto en NoteFlow
-23%
Tickets por conflictos
↓ Objetivo: 0%
<50ms
Latencia sync local
↑ vs 5000ms (polling)
Merges sin conflicto
Garantizado por CRDT
~80KB
Bundle size añadido
↓ vs límite 150KB
📦 Dependencias Instaladas
yjs Core CRDT ~35KB
y-websocket Sync provider ~12KB
y-indexeddb Offline cache ~8KB
@tiptap/extension-collaboration Editor binding ~10KB
@tiptap/extension-collaboration-cursor Cursores color ~8KB
y-mongodb-provider Persistencia Atlas ~7KB (server)
Reglas Aplicadas
1 Y.Text para el documento rich-text, Y.Map para metadatos del doc (título, tags)
2 Awareness para cursores y presencia — ephemeral, no en MongoDB
3 y-indexeddb siempre activo — protege a Laura sin código extra
4 Auth por JWT token en params del WebSocket — rechaza conexiones no autorizadas antes del sync
5 history: false en StarterKit — Tiptap cede el undo/redo a Yjs (colaborativo)
6 Documentos separados por documentId — un room WebSocket = un doc, escalabilidad horizontal