Los documentos compartidos por chat o por email se duplican, se pisan y se pierden, y nadie sabe quién cambió qué. Docs Workspace es un espacio de documentos multi-organización: cada equipo crea su organización, invita a personas con un rol y escribe en conjunto en un editor de texto enriquecido.
Varias personas pueden escribir en el mismo documento a la vez y ver los cursores de las demás. Cada documento conserva un historial de versiones inmutable. Lo que cada persona puede ver o hacer depende de su rol, y las reglas se aplican en la base de datos, no solo en la interfaz, así que nadie puede saltárselas llamando a la API directamente.
Por qué importa
- Colaborativo: varias personas editan el mismo documento en vivo, sin el clásico "¿quién tiene la última copia?".
- Con trazabilidad: cada documento tiene un historial numerado e inmutable, con autor y una descripción opcional.
- Controlado: tres roles (admin, editor y member) deciden quién puede leer, editar, versionar, invitar o eliminar, y la base de datos lo hace cumplir.
- Aislado: los datos de cada organización son invisibles para las demás, a nivel de base de datos.
- Acotado: los límites por organización (documentos, usuarios invitados, versiones y organizaciones por usuario) son configurables y se aplican en el servidor.
- Portable: los documentos se guardan como Markdown y se pueden descargar como
.mden cualquier momento.
Qué hace
Organizaciones, roles e invitaciones
- Crear organizaciones, cambiar entre ellas desde una barra lateral y renombrarlas o eliminarlas (admin). La URL (
/org/<slug>) siempre indica cuál está abierta. - Invitar por email eligiendo el rol, cambiar el rol de un miembro, quitarlo o revocar una invitación pendiente.
- Quien ya tiene cuenta acepta la invitación desde su panel; quien no la tiene, registrándose desde el enlace del email.
- Salvaguardas: un admin no puede cambiar ni quitar su propia membresía, y no se puede degradar al último admin.
Los tres roles
- Admin: crea y elimina documentos, edita, crea versiones y administra miembros y ajustes de la organización.
- Editor: edita y renombra cualquier documento y crea versiones. No crea ni elimina documentos ni gestiona miembros.
- Member: lee documentos y versiones. Ve el documento en modo solo lectura, que igualmente se actualiza en vivo mientras otros escriben.
Editor
- Texto enriquecido con títulos, negrita, cursiva, tachado, código en línea, listas, citas y bloques de código.
- Colaboración en tiempo real: escritura simultánea, cursores remotos con nombre, lista de presencia y deshacer/rehacer por usuario.
- Vista previa de solo lectura y descarga del documento como
.md. - El estado de conexión y de guardado siempre está visible, y el editor sigue funcionando si se cae la conexión en vivo.
Historial de versiones
- Crear una versión (admin y editor) con una descripción opcional. El contenido lo copia la base de datos, por lo que no se puede falsificar.
- Lista de versiones con número, descripción, autor y fecha, y visor de solo lectura con descarga en
.md. - Las versiones son inmutables y solo desaparecen cuando se elimina su documento.
Cómo funciona la edición colaborativa
La colaboración usa Yjs, una estructura de datos de tipo CRDT que fusiona ediciones simultáneas sin conflictos. Un proveedor propio transporta los cambios sobre Supabase Realtime, y Postgres guarda el registro de cambios para que el documento sea duradero y recuperable.
- Cuando alguien escribe, el cambio se transmite al instante por un canal privado del documento (
doc:<id>), y solo pueden enviar admins y editores. - Los demás editores lo aplican de inmediato.
- Cada editor agrupa sus cambios y los guarda en la base cada pocos segundos, y poco después actualiza la copia en Markdown del documento.
- Quien abre el documento carga el registro guardado y luego pide a los editores conectados lo que todavía no se guardó.
Algunos detalles de robustez:
- Compactación: cuando se acumulan más de 100 filas de cambios, un editor las reemplaza por una única instantánea.
- Pegados grandes: un cambio demasiado grande para un mensaje en vivo se guarda en la base y se avisa a los demás, que lo leen de allí.
- Resiliencia: el cliente renueva su token cada 30 segundos, reconstruye el canal si el servidor lo cierra y, si el canal en vivo no está disponible, vuelve a leer el registro guardado cada pocos segundos. Editar nunca depende de la conexión en vivo.
Decisiones de diseño
- La organización siempre sale de la sesión, nunca del cliente. Además, las Server Actions reciben la organización que mostraba la página y se niegan a actuar si ya no es la activa, de modo que una segunda pestaña no puede hacer que una acción caiga en la organización equivocada.
- Las reglas de negocio viven en la base de datos (triggers, privilegios por columna y políticas), así que se cumplen sin importar qué cliente llame a la API.
- La URL es la fuente de verdad de la organización activa. El proxy activa la organización de
/org/<slug>y cada página comprueba que URL y sesión coincidan. - El Markdown se renderiza a través del modelo de documento del editor, nunca como HTML crudo.
- Sin clave de servicio en la aplicación. Todo corre con el token del propio usuario.
Clerk administra usuarios, organizaciones, roles, permisos e invitaciones. Supabase guarda documentos, versiones e historial de ediciones. Supabase sabe quién es el usuario y qué organización tiene activa a partir del token de Clerk, de modo que el navegador y el servidor consultan la base con las mismas reglas.
Seguridad
Defensa en profundidad: cada operación sensible se verifica en varios niveles independientes.
- Guardas de servidor: verifican la sesión, la organización activa y el permiso o rol de Clerk en cada página y cada Server Action. Una URL que no coincide con la organización de la sesión responde 404.
- Server Actions: vuelven a comprobar permisos, validan y normalizan cada entrada, toman la organización de la sesión y devuelven errores genéricos.
- Row Level Security: cada tabla está limitada a la organización del token y al rol de quien consulta. Sin token, sin organización activa o con un rol desconocido, no se ve ni se cambia nada.
- Privilegios por columna: un cliente nunca puede fijar autores, fechas, números de versión, el slug ni el contenido de una versión.
- Triggers: el servidor asigna autor y fechas, numera cada versión, copia el documento en la instantánea y hace cumplir cada límite bajo un bloqueo, de modo que las peticiones concurrentes no pueden esquivarlo.
- Autorización de Realtime: los canales colaborativos son privados y están gobernados por políticas. Cualquier otro nombre de canal se deniega.
- Roles dos veces: el servidor verifica los permisos personalizados de Clerk y RLS verifica el rol del token. Ambos deben coincidir.
- Cabeceras de seguridad:
nosniff, anti-framing, políticas de referrer y permisos, HSTS en producción y una Content-Security-Policy lista para aplicar.
Otras protecciones:
- Los datos de presencia de otros usuarios (nombre, color del cursor) se validan y se insertan como texto; nada que llegue por la red se renderiza como HTML.
- El Markdown inseguro (etiquetas script, enlaces
javascript:odata:, manejadores de eventos) no sobrevive al renderizado. - Los mensajes de otros clientes en el canal en vivo se validan y los mal formados se ignoran.
- Nunca se informa si una organización o documento existe cuando la persona no tiene permiso para verlo.
Límites por organización
- Organizaciones por usuario: 3.
- Documentos por organización: 20.
- Usuarios invitados por organización: 5 (pendientes y aceptados).
- Versiones por documento: 100.
- Contenido de un documento: 500.000 caracteres.
Cada límite se aplica donde corresponde: en un trigger de la base de datos cuando es posible y, para los de Clerk, en el servidor con el ajuste de Clerk como respaldo atómico.
Verificación
Cuatro scripts SQL simulan tokens de Clerk con distintos roles y organizaciones e intentan romper las reglas. Se ejecutan después del esquema, hacen rollback al final y no dejan datos. Cada uno termina con ALL TESTS PASSED, y cualquier fallo indica qué regla se rompió.
documents_rls.sql(45 comprobaciones): acceso anónimo y sin rol, permisos por rol, aislamiento entre organizaciones, columnas inmutables, autores fijados por el servidor, slugs, límites, numeración de versiones y cascadas.documents_realtime.sql(17): quién puede recibir y enviar en los canales de cada documento.document_updates_rls.sql(18): permisos del registro de ediciones, validación, límite de filas y compactación.version_limit.sql(4): el tope de versiones y su configurabilidad.
En total son 84 comprobaciones automatizadas de base de datos. El código TypeScript es estricto y compila sin errores.
Stack
- Next.js 16 (App Router, Server Components, Server Actions,
proxy.ts) y React 19. - TypeScript estricto, Tailwind CSS v4 y shadcn/ui con primitivos de Base UI.
- TipTap 3 con Yjs para la edición colaborativa.
- Clerk: usuarios, organizaciones, roles, permisos e invitaciones.
- Supabase: PostgreSQL, Row Level Security y Realtime (Broadcast, Presence y Postgres Changes).
- Módulos de Clerk generados con
@dariomvg/create(basic-auth,organizationsyauthorization). Las guardas, el mapeo de roles, la verificación de la organización activa y toda la interfaz son propios.
Alcance y límites conocidos
- No hay límite de frecuencia de peticiones: el volumen de datos está acotado, pero hacerlo requeriría un almacén externo como Redis.
- La revocación de acceso en un canal en vivo ya abierto no es instantánea: se aplica cuando el token se renueva, en aproximadamente un minuto.
- Todavía no hay restauración de versiones, ni mover documentos entre organizaciones, ni paginación.
- Si se cierra la pestaña justo después de escribir y nadie más está conectado, el último segundo de escritura puede perderse.
Lo que demuestra este proyecto
- Multi-tenancy real, con aislamiento entre organizaciones garantizado por la base de datos.
- Edición colaborativa con CRDT, sobre una infraestructura que sigue funcionando cuando la conexión en vivo falla.
- Integración entre dos servicios (identidad en Clerk, datos en Supabase) con una sola fuente de verdad para los permisos.
- 84 pruebas de seguridad que se pueden ejecutar y volver a ejecutar.
