Skip to content
3 min

Module boundaries and relationships

Understand the responsibilities, data flow, and assembly of the Collaboration Client, Transport, Endpoint, Service, and Database Adapter.

Univer Collaboration combines a browser Client with a set of server-side modules. Each module owns one class of responsibility. Applications compose them through public interfaces without entering the internal OT, protocol, or persistence implementation.

Complete module diagram

Collaboration Client

The Browser Collaboration Client runs beside the Univer Runtime SDK and:

  • requests Unit snapshots and missing confirmed changesets;
  • obtains a one-time Session Ticket and opens a WebSocket;
  • joins Unit Rooms and exchanges Presence;
  • submits local mutations as changesets;
  • receives ACKs and broadcasts, then fills missing changes after reconnection.

The Client does not decide whether a user is trusted and is not the authoritative data source. The server still enforces identity and authorization through the appropriate Middleware.

Node Transport

@univerjs-pro/collaboration-transport-node is the Node.js HTTP/WebSocket entry point. It receives requests and WebSocket upgrades from the host HTTP server, runs application entry Middleware, and dispatches traffic to Endpoints.

The Transport does not understand OT, Unit data, or ACL models. It exposes three primary assembly methods:

TypeScript
transport.use(httpMiddleware);
transport.useUpgrade(upgradeMiddleware);
transport.register(endpoint);
  • use() participates in ordinary HTTP requests and can establish userID, customData, logs, or traces;
  • useUpgrade() participates in the WebSocket handshake and can reject it before connection;
  • register() registers a protocol Endpoint and gives the Transport ownership of its lifecycle.

Collaboration Endpoint

@univerjs-pro/collaboration-endpoint implements the protocol used by Univer Collaboration Client and owns the following capabilities:

CapabilityEndpoint responsibility
Content loadingProvides Unit snapshots, blocks, and missing confirmed changesets to the Client
Change submissionReceives changesets, returns ACKs, and broadcasts confirmed changesets to the Room
Session connectionIssues one-time Session Tickets and establishes and maintains WebSocket Sessions
Room collaborationHandles JOIN, LEAVE, member state, and Presence
Unit lifecycleMaps delete and recovery requests to the corresponding Service calls

The Endpoint does not perform OT or store collaboration data directly. It calls the Service for authoritative results and maps those results back to the Client protocol. Product operations such as creating a document should call the Service from an application API.

Collaboration Service

@univerjs-pro/collaboration-service is the network-independent collaboration core for Sheet, Doc, Slide, Board, and Base. It:

  • reads Unit recovery data and confirmed changesets;
  • creates Units from Unit Data or Snapshots;
  • submits changesets, performs OT, and advances continuous revisions;
  • identifies duplicate submissions through (unitID, sid, reqId);
  • soft-deletes, recovers, or permanently deletes Units;
  • runs Service Middleware and emits Events after confirmation.

The Service does not provide HTTP, WebSocket, users, roles, ACLs, hierarchy, or file management. The Endpoint uses it to implement the browser protocol. Applications can also call its public API directly from business APIs or background tasks.

Core APIs

CapabilityService API
Load a UnitgetUnitLoadData(), getUnitLoadDataWithBlocks()
Read incremental datagetChangesets(), getSheetBlock()
Create a UnitcreateUnitFromData(), createUnitFromSnapshot()
Submit changessubmitChangeset()
Delete and recoverdeleteUnits(), recoverUnits()

Database Adapter

The Database Adapter is the contract between the Service and storage. It must guarantee:

  • atomic creation of an initial snapshot and its dependencies;
  • revision compare-and-swap;
  • idempotent changeset submissions;
  • snapshot visibility only after all dependencies exist;
  • all-or-nothing Unit lifecycle batches.

The SDK provides Memory and SQLite implementations. Applications that need another database should implement the same contract. See Database Adapters.

Four core concepts

ConceptMeaning
UnitIndependently loadable and collaborative Univer content identified by a unique unitID and type
SnapshotPersistent Unit content at a revision, not a mutable live object
ChangesetA submitted mutation collection with a base revision and idempotency identity
RevisionThe continuous version number of confirmed Unit state; the initial value is 1

Running content must be changed through Facade APIs or Commands. Mutating a snapshot object does not update a live Unit in the Client or Service.

Loading a Unit

The userID/customData established by Transport Middleware follows the current HTTP request into Service Middleware. Application policy decides whether reading is allowed; joining a Room is not a prerequisite for direct HTTP reads.

Submitting an edit

Text
Facade / Command
  → Mutation
  → Client Changeset
  → Endpoint submit
  → Service Middleware
  → OT + Revision CAS
  → Adapter atomic commit
  → confirmed Changeset
  → ACK / Room broadcast

The confirmed revision in the Database Adapter is authoritative. ACKs and broadcasts keep online clients synchronized with low latency. A client that misses a message can retrieve confirmed changesets using its known revision.

Minimal server assembly

TypeScript
import { createServer } from "node:http";

import { MemoryDatabaseAdapter } from "@univerjs-pro/collaboration-database-memory";
import { UniverCollabEndpoint } from "@univerjs-pro/collaboration-endpoint";
import { UniverCollabService } from "@univerjs-pro/collaboration-service";
import { createNodeTransport } from "@univerjs-pro/collaboration-transport-node";

const database = new MemoryDatabaseAdapter();
const service = new UniverCollabService({ dbAdapter: database });
const endpoint = new UniverCollabEndpoint(service);
const transport = createNodeTransport();

transport.use(async (context, next) => {
  context.userID = "demo-user";
  await next();
});
transport.register(endpoint);

const server = createServer((request, response) => {
  transport.handleRequest(request, response);
});
server.on("upgrade", (request, socket, head) => {
  transport.handleUpgrade(request, socket, head);
});
server.listen(3010);

The fixed user and Memory Adapter demonstrate module assembly only. Applications establish real Context through Middleware and select an Adapter for their data requirements.

Create a Unit through an application API

An application can define its own HTTP or RPC endpoint and call the service created above after authentication, input validation, and business checks:

Text
Business client
  → POST /api/units
  → application authentication and validation
  → service.createUnitFromData()
  → Database Adapter

The following Express example assumes authentication Middleware has set response.locals.user and that request.body.data has already been validated as Workbook Data:

TypeScript
import { randomUUID } from "node:crypto";

import { json } from "express";
import { UniverType } from "@univerjs/protocol";

app.post("/api/units", json({ limit: "1mb" }), async (request, response, next) => {
  try {
    const user = response.locals.user as { readonly userID: string };
    const unitID = randomUUID();

    const result = await service.createUnitFromData(
      {
        type: UniverType.UNIVER_SHEET,
        data: {
          ...request.body.data,
          id: unitID,
          rev: 1,
        },
      },
      {
        userID: user.userID,
        customData: { traceID: randomUUID() },
      },
    );

    response.status(result.status === "created" ? 201 : 200).json(result);
  } catch (error) {
    next(error);
  }
});

POST /api/units belongs to the application; it is not a Collaboration Client protocol route. Calling createUnitFromData() still runs the Service's createUnit Middleware. Product records, owner ACLs, error-response mapping, and failure compensation remain application concerns.

Resource cleanup

transport.dispose() disposes Endpoints registered through register(). The application disposes the Service and its injected Database Adapter separately. Use the reverse assembly order:

TypeScript
await transport.dispose();
await service.dispose();
await database.dispose();

Continue with Middleware and Events to extend behavior at these module boundaries.