Backend Integration Patterns with SpreadJS Collaboration Server

Posted by: eberridge on 8 February 2026, 5:09 pm EST

    • Post Options:
    • Link

    Posted 8 February 2026, 5:09 pm EST

    Hello Mescius Support Team,

    We’re in the process of adopting the new SpreadJS Collaboration Server and would appreciate guidance on recommended backend integration patterns, especially where collaboration intersects with server-side spreadsheet introspection and automated updates.

    High-level architecture

    Our application is hosted in Azure and consists of:

    • Frontend: React SPA using:
      • SpreadJS
      • SpreadJS Designer Component
    • Backend API: .NET / EF Core
      • PostgreSQL (Npgsql)
      • HotChocolate GraphQL + REST endpoints
      • Azure Service Bus listeners for async/batch workflows
    • Simulation system:
      • Multiple .NET microservices
      • Workflow orchestration via Service Bus topics/subscriptions
      • Simulation results written to Blob Storage, with completion events sent via Service Bus

    Pre-collaboration approach

    Before adopting collaboration:

    • Each spreadsheet was stored as SpreadJS SJS binary (
      bytea
      ) in PostgreSQL via EF.
    • On demand, the .NET API:
      • Loaded the SJS
      • Opened it using Document Solutions for Excel (DsExcel)
      • Read (and occasionally wrote) spreadsheet data programmatically
    • Simulation completion handlers (Service Bus processors in .NET) would:
      • Load the spreadsheet
      • Insert simulation results using DsExcel
      • Save the updated SJS back to the database

    Current collaboration setup

    We have now implemented:

    • SpreadJS Collaboration Server (Node.js) with:
      • PostgreSQL backend (separate database/schema from our .NET API)
      • Default OT snapshot + operation storage as documented
    • React client using SpreadJS Designer connected directly to the collaboration server

    In this new model:

    • The spreadsheet SJS is no longer stored in our .NET database
    • The collaboration server owns persistence via its snapshot/operation model

    Challenges we’re trying to solve

    We have two primary concerns and want to align with best practices rather than building against internal assumptions.


    1. Backend introspection / read-only access

    Our .NET API still needs to load and introspect spreadsheets to extract structured data (tables, named ranges, calculated values, etc.).

    Previously, DsExcel operated directly on stored SJS files.

    With collaboration:

    • The persisted format is a collaboration snapshot, not SJS or SSJSON
    • DsExcel cannot open collaboration snapshots directly

    Questions:

    • Is the recommended approach to:
      • Export the current collaboration document to SJS / SSJSON / XLSX via the collaboration server and then consume that in DsExcel?
    • Are there supported or recommended APIs/patterns for server-side “read-only” access to collaboration documents?
    • Is running SpreadJS headlessly in Node to load a snapshot and extract values considered a supported approach?

    2. Automated spreadsheet updates from simulation workflows

    When simulations complete, their results must be inserted into the spreadsheet automatically.

    Previously:

    • This was done in .NET via Service Bus listeners using DsExcel.

    With collaboration:

    • The spreadsheet is now owned by the collaboration server
    • Writing directly to the collaboration database is clearly undesirable

    Questions:

    • Is the recommended approach to:
      • Apply updates as collaboration operations (e.g., via a “bot” or server-side client that joins the document and writes data)?
    • Is there guidance or examples for server-side, non-interactive updates to collaborative spreadsheets?
    • Would you recommend moving Service Bus processing related to spreadsheet updates into the Node collaboration service, or exposing an API on the collaboration service that applies updates on behalf of other backend systems?

    What we’re trying to avoid

    • Writing directly to collaboration PostgreSQL tables
    • Relying on undocumented snapshot formats
    • Designing a workflow that will break with future collaboration updates

    What we’re looking for

    • Confirmation of supported / recommended integration patterns
    • Clarification on how Mescius envisions:
      • Backend reads
      • Backend writes
      • Automated (non-user) edits

        in a collaborative SpreadJS environment
    • Any relevant documentation, samples, or architectural guidance we may have missed

    We’re happy to share simplified code samples or diagrams if that helps clarify our use case.

    Thanks in advance for your help — we’re excited about collaboration support and want to make sure we’re building on it correctly.

    Best regards,

    Eric

  • Posted 9 February 2026, 8:49 am EST

    Hi,

    We are still investigating the issue at our end. We will let you know about our findings as soon as possible.

    Regards,

    Priyam

  • Posted 10 February 2026, 5:01 am EST

    Hi Eric,

    Thank you for reaching out with such a detailed explanation of your architecture and requirements.

    Understanding Your Situation

    You’re correct in identifying the key architectural shift: previously, you stored SJS binary files in PostgreSQL, and your .NET services used DsExcel to read/write directly to those files. Now, with the SpreadJS Collaboration Server, the persistence format has changed to collaboration snapshots (operational transform-based), which DsExcel cannot natively process.

    Recommended Solution

    The officially supported approach is to use a headless browser on the collaboration server to convert snapshots to standard Excel formats (XLSX, SJS, or SSJSON). This allows you to maintain your existing .NET service workflows with minimal changes.

    How This Works

    1. For Backend Reads (Introspection):

    When your .NET API needs to introspect a spreadsheet, it calls an export endpoint on your collaboration server

    The collaboration server uses a headless browser (Puppeteer or Playwright) to:

    Load the collaboration snapshot into SpreadJS

    Export to XLSX, SJS, or SSJSON format

    Return the file to your .NET API

    Your .NET services continue using DsExcel exactly as before

    1. For the Complete Collaboration Lifecycle:

      You can design a workflow that bridges both worlds:

      When collaboration starts (first user opens):
    2. Load stored SJS file from your PostgreSQL database
    3. Import into SpreadJS on the client
    4. Create collaboration snapshot from the imported workbook
    5. Multiple users collaborate in real-time

      When collaboration ends (last user leaves):
    6. Detect room closure via destroyRoom hook
    7. Export collaboration snapshot to SJS using headless browser
    8. Store the updated SJS file back to your PostgreSQL database
    9. Clear collaboration state

      Result: Your database continues to store SJS files in the same format as before, and all your existing .NET services (including Service Bus handlers for simulation updates) work unchanged.

    For Simulation Updates

    Batch Updates (Simpler):**

    Continue your existing Service Bus workflow in .NET

    When simulations complete, update the stored SJS file using DsExcel

    Next time users open the spreadsheet, they see the updated data

    Reference Implementation

    I’ve attached a working sample (spreadjs_collaboration.zip) that demonstrates this exact pattern:

    Files are stored in a files folder (equivalent to your PostgreSQL SJS storage)

    When the first user opens a document, it loads from the file and creates a collaboration snapshot

    Users can collaborate in real-time

    When all users leave the room, the collaboration snapshot is exported back to an Excel file using a headless browser

    The exported file is saved back to the files folder

    When users return, the process repeats with the updated file

    Key implementation points in the sample:

    Server-side headless export service

    destroyRoom hook monitoring

    Automatic file persistence after a configurable delay (e.g., 30 seconds to allow for reconnections)

    Re-initialization from stored files on next access

    Sample: spreadjs_collaboration.zip

    Best regards,

    Priyam

  • Posted 10 February 2026, 1:08 pm EST

    Hello Priyam,

    Thank you for the detailed explanation and the reference implementation — it was very helpful, and we understand the lifecycle-based approach you’re proposing.

    After reviewing it carefully, we do have a concern with this model in our environment and wanted to ask whether an alternative pattern could be considered supported.

    Our concern with the proposed lifecycle-based approach

    In your recommended solution, collaboration is treated as a bounded session:

    • SJS is the system of record
    • Collaboration is transient
    • Backend automation (simulations) updates SJS only when no users are present
    • Collaboration state is exported back to SJS when the last user leaves

    While this is clear and well-defined, it introduces a challenge for us:

    • Our simulations complete asynchronously and independently of user presence
    • Simulation results may be ready while users are actively collaborating
    • Queuing or deferring those results until all users leave a room is not ideal for our use case
    • From a user-experience perspective, we would strongly prefer simulation results to appear in near–real time while users are editing. I provided simulations as an example of backend-initiated updates, but we have other scenarios in which such updates are required. Blocking and queuing for all of them will become untenable.

    Because of this, we’re looking for a way to safely apply automated updates during an active collaboration session.

    Alternative approach we are considering

    We’d like to explore whether the following pattern could be supported:

    • Simulation completion events are delivered to the collaboration service
      • Either by the service subscribing directly to Azure Service Bus
      • Or via an API call from our .NET backend
    • A server-side, headless SpreadJS instance (or similar automation client):
      • Joins the collaboration room
      • Behaves like a normal participant (no special database access)
      • Applies updates using standard SpreadJS APIs
    • Those updates flow through the collaboration framework as normal operations
    • All connected users immediately see the results

    Conceptually, this would treat automated processes as “bot users” rather than out-of-band file mutations.

    Our questions for you

    1. Is a server-side “bot participant” model — where automated updates are applied as collaboration operations — something that SpreadJS Collaboration Server is designed to support?

    2. Are there any technical or architectural reasons this approach would be unsafe or unsupported (for example, around OT consistency, snapshot integrity, or concurrency)?

    3. If this pattern is not officially supported today:

      • Is there a recommended way to achieve near–real time automated updates during active collaboration?
      • Or is the lifecycle-based export/import model the only supported mechanism?
    4. Are there any samples, documentation, or guidance (even high-level) around:

      • Server-side, non-interactive participants
      • Applying updates programmatically during active collaboration sessions

    What we want to avoid

    Just to be explicit, we are not looking to:

    • Write directly to collaboration database tables
    • Bypass OT or snapshot mechanisms
    • Depend on undocumented snapshot formats

    We’re specifically trying to stay within supported APIs and architectural patterns.


    We appreciate that collaboration introduces new constraints, and we’re happy to adapt — we just want to ensure we choose a design that scales well with asynchronous, long-running simulation workflows while remaining aligned with SpreadJS’s intended usage.

    Thanks again for your help, and we look forward to your guidance.

    Best regards,

    Eric Berridge

  • Posted 11 February 2026, 8:03 am EST

    Hi,

    We are not certain whether the “bot participant” approach is supported or if there is a better way to achieve this requirement, so we have consulted the development team for clarification. The internal tracking ID for this case is SJS-33816. We will share an update as soon as we receive more information.

    Regards,

    Priyam

  • Posted 14 February 2026, 12:44 pm EST - Updated 14 February 2026, 3:15 pm EST

    Hello Priyam,

    Following up on this thread and SJS-33816 — we went ahead and implemented the “bot participant” approach I described in my second post, and I’m happy to report that it works very well. I wanted to share our findings with you.

    Summary

    We now have a working system where:

    • Multiple users collaborate on a SpreadJS workbook in real time (browser clients)
    • Backend automation processes also modify the same workbook — during active collaboration — by joining the room as a headless “bot” participant
    • All changes (human and automated) flow through the standard OT pipeline, so consistency and conflict resolution are handled naturally
    • Users see automated results appear in the spreadsheet in near-real time without page reloads

    Architecture

    We run the SpreadJS Collaboration Server as a standalone Node.js service with PostgreSQL persistence (as documented). Alongside it, we built a small automation module that is responsible for applying backend-driven updates.

    How the automation works:

    1. An external trigger (e.g., a Service Bus message, an API call, a cron job, etc.) signals that data needs to be written into a particular workbook.
    2. The automation module launches a headless Chromium browser page via Playwright.
    3. SpreadJS core and the collaboration addon are injected into the page via
      page.addScriptTag()
      .
    4. A bundled automation script is also injected — this contains the collaboration client constructors (
      Client, SharedDoc, TypesManager, bind, type from the @mescius/js-collaboration-* and @mescius/spread-sheets-collaboration-client packages
      ) exposed as globals, plus the business logic for the data manipulation.
    5. The headless page connects to the collaboration server via WebSocket, joining the room using a “bot” user identity. It behaves as a normal collaboration participant.
    6. The automation script runs inside
      page.evaluate()
      — it uses standard SpreadJS APIs (
      sheet.setValue(), sheet.tables.resize(), etc.
      ) to apply the updates. All mutations are wrapped in
      workbook.collaboration.startBatchOp() / workbook.collaboration.endBatchOp()
      so the entire set of changes is submitted as a single atomic batch of OT operations, which improves both performance and reliability.
    7. Because the page is bound to the collaboration doc via
      bind(workbook, doc)
      , all mutations are captured as OT operations and broadcast to any connected users immediately.
    8. Once the automation finishes, the doc and connection are destroyed, and the page (and browser context) is closed.

    Key implementation decisions:

    • Shared browser instance: We keep a single shared Chromium browser process alive and create a new
      BrowserContext + Page
      per automation run. This avoids the ~1-2 second cold-start cost of launching Chromium on every trigger.
    • Script injection order matters: SpreadJS core must load before the collaboration addon, which must load before any automation code that uses
      GC.Spread.Sheets
      .
    • Batch operations: All automated mutations are wrapped in
      workbook.collaboration.startBatchOp() / endBatchOp()
      . This ensures the full set of changes is captured as a single coherent batch of operations rather than firing individual OT ops per cell, which significantly reduces network overhead and avoids partial-update states for connected users.
    • Bot user identity: We create a minimal user object with a fixed identifier (e.g.,
      { id: "automation-bot", name: "Automation" }
      ) and assign it
      BrowsingMode.edit
      permission. The collaboration server sees this as a normal participant.
    • No special server APIs required: The automation module connects to the collaboration server over the same WebSocket endpoint that browser clients use. No custom HTTP endpoints, no database writes, no snapshot-format parsing.
    • Server-side bootstrap: If the automation triggers before any user has opened the workbook, the collaboration document may not exist yet. Our server middleware detects this (empty snapshot on
      readSnapshots
      ) and eagerly bootstraps a default workbook snapshot via
      documentServices.submit()
      with a
      create
      op. This means automation can run at any time — it doesn’t depend on a user having opened the document first.

    What we like about this approach

    • OT handles everything. We don’t touch the collaboration database, parse snapshots, or bypass any layer. The headless browser is just another client.
    • Real-time visibility. Users see automated updates appear live, just as if another user were typing.
    • No session lifecycle coupling. Automation runs independently of whether users are connected or not. No need to queue, defer, or coordinate with room open/close events.
    • Standard SpreadJS APIs only. The automation code inside
      page.evaluate()
      uses the same SpreadJS APIs you’d use in a browser. Nothing undocumented.
    • Scalable. Multiple automation runs can target different rooms concurrently (different browser contexts in the same Chromium process).

    Trade-offs

    • Headless browser overhead: Each automation run does spin up a browser page. In practice, with a shared Chromium instance, per-run overhead is modest (a few hundred ms to create a page, connect, and inject scripts). For our workload (infrequent, event-driven triggers) this is more than acceptable.
    • Node.js dependency: The automation module runs in Node.js alongside the collaboration server since it needs Playwright and SpreadJS. If your automation triggers originate in a different stack (e.g., .NET Service Bus processors), you’d need a thin API or message bridge to dispatch those triggers to the Node service.

    Simplified example flow

    [External Trigger]
           │
           ▼
    [Automation Module (Node.js)]
           │
           ├── Get/create shared Chromium browser
           ├── Create new BrowserContext + Page
           ├── page.goto(automationPageUrl)        // blank HTML page served by the collab server
           ├── page.addScriptTag(spreadCore)       // gc.spread.sheets.all.min.js
           ├── page.addScriptTag(collabAddon)      // gc.spread.sheets.collaboration.min.js
           ├── page.addScriptTag(automationBundle) // bundled: Client, SharedDoc, TypesManager, bind, type
           │
           ├── page.evaluate(async (args) => {
           │
           │     // --- Register the SpreadJS OT type ---
           │     TypesManager.register(type);
           │
           │     // --- Create workbook ---
           │     const workbook = new GC.Spread.Sheets.Workbook(hostElement);
           │
           │     // --- Connect to collaboration server ---
           │     const client = new Client(serverUrl);
           │     const connection = client.connect(roomId, { auth: botUser });
           │     const doc = new SharedDoc(connection);
           │
           │     // --- Fetch the OT document ---
           │     // If the doc doesn't exist yet, server-side middleware on the
           │     // `readSnapshots` hook detects the empty snapshot and bootstraps
           │     // a default workbook via documentServices.submit() with a create op.
           │     // This means doc.fetch() always returns a valid document.
           │     await doc.fetch();
           │
           │     // --- Bind workbook to the collaboration doc ---
           │     bind(workbook, doc);
           │
           │     // --- Apply data changes as a batch ---
           │     workbook.collaboration.startBatchOp();
           │     try {
           │       const sheet = workbook.getSheetFromName("MySheet");
           │       // ... apply data using standard sheet/table APIs ...
           │     } finally {
           │       workbook.collaboration.endBatchOp();
           │       await delay(5000); // allow ops to flush before teardown
           │     }
           │
           │     // --- Cleanup ---
           │     doc.destroy();
           │     connection.destroy();
           │
           │   }, { serverUrl, roomId, botUser, data })
           │
           └── page.close()
    

    Conclusion

    The “bot participant via headless browser” pattern gives us exactly what we described in my earlier post: safe, real-time, automated updates to collaborative workbooks without bypassing any collaboration layer. It uses only documented SpreadJS and collaboration APIs.

    We’d love to hear from the development team (re: SJS-33816) whether there are any concerns with this approach from an OT consistency or supportability standpoint, or whether this is something you’d consider endorsing as a recommended pattern.

    Thanks again for your help throughout this thread.

    Best regards,

    Eric

  • Posted 16 February 2026, 6:14 am EST

    Hi,

    Your proposed approach looks good to us, and the development team has confirmed the following:

    Regarding: Is This Pattern Officially Supported?

    Yes, your idea is perfectly feasible and represents a recommended approach.

    It is important to avoid modifying collaboration snapshots directly in the database. The system manages concurrency conflicts, message broadcasting, real-time synchronization, and eventual consistency only when changes are submitted as operations (ops).

    As long as updates are performed by submitting ops through the collaboration mechanism, snapshot integrity and OT consistency will be maintained.

    A minor optimization would be to submit ops directly via the server-side API. Compared to using the client-side API, this avoids additional overhead such as network transmission and message compression.


    Sample: Robot Writing Key Code

    Below is a sample demonstrating how a server-side “bot” can modify a collaborative document properly by generating and submitting ops:

    export async function modifyDocument(documentServices, docId) {
        // Fetch the latest document snapshot to ensure modifications are based on the current version
        const snapshot = await documentServices.fetch(docId);
    
        // Create a temporary Workbook and load the snapshot content
        const workbook = new GC.Spread.Sheets.Workbook();
        workbook.collaboration.fromSnapshot(snapshot.data);
    
        // Register changeSet listener: when API is called, SpreadJS generates corresponding ops.
        // This callback collects those ops for later submission to the collaboration server.
        const collectedChangesets = [];
        workbook.collaboration.onChangeSet((changeSet) => {
            collectedChangesets.push(changeSet);
        });
    
        // Use startBatchOp/endBatchOp to combine multiple setValue operations into a single batch.
        workbook.collaboration.startBatchOp();
        const sheet = workbook.getActiveSheet();
        sheet.setValue(0, 0, "hello");
        sheet.setValue(0, 1, "world!");
        workbook.collaboration.endBatchOp();
    
        const src = randomUUID();
    
        // Submit collected changeSets to the collaboration server one by one
        // Each op carries: src (source ID), seq (sequence number, starting from 1), v (document version it's based on)
        for (let i = 0; i < collectedChangesets.length; i++) {
            const op = collectedChangesets[i];
            const seq = i + 1;
            await documentServices.submit(docId, { src, seq, v: snapshot.v + i, op });
        }
    
        // Destroy the temporary Workbook to free resources
        workbook.destroy();
    }

    This pattern ensures:

    • OT consistency
    • Proper conflict handling
    • Real-time synchronization for connected users
    • No direct database manipulation

    Regarding: Documentation / Guidance

    The development team has also created a demo simulating a Weather Query Bot scenario:

    • Users open a collaborative spreadsheet in the browser and list city names in Column A.
    • A server-side bot reads the list of cities every 5 seconds.
    • The bot calls a weather service API to retrieve data.
    • It writes temperature, feels-like temperature, and time into Columns B, C, and D.
    • The updated data is synchronized and displayed in real-time for all connected users.

    This demonstrates how automated server-side participants can safely interact with collaborative documents while preserving consistency.

    Sample: spread-collaboration-demo.zip

    Regards,

    Priyam

  • Posted 16 February 2026, 11:23 am EST - Updated 16 February 2026, 4:22 pm EST

    Hello Priyam,

    Thank you for working through this with me. I’m happy to have a supported solution (two actually) that checks all the boxes for us. Your suggested improvement to use the server-side API will be a solid improvement for us, considering that the backend modifications will be running in the same NodeJS process as the server. It will be good to get rid of the Playwright dependency. Our only concern is potential fragility introduced by using node-browser shims. I’ll take some time to fully explore this and see which is preferable for our scenarios.

    • Edit: I like the server-side API approach and have implemented it. It’s more direct and has a lot of advantages. The only drawbacks are 1) possible breaking changes in future versions of Mescius libraries, and 2) that the node-canvas shim will require additional native packages in our container image. I think the benefits outweigh the costs.

    Again, thank you very much. Please feel free to resolve SJS-33816 at your convenience.

    All the best!

    Eric

  • Posted 17 February 2026, 5:58 am EST

    Hi Eric,

    Thank you for the update and for taking the time to thoroughly evaluate both approaches!

    I’m glad to hear that the server-side API solution is working well for you and that you’ve successfully implemented it. You’re absolutely right that running everything in the same Node.js process without external dependencies like Playwright is a cleaner architecture.

    Regarding Your Concerns of Breaking Changes:

    We maintain backward compatibility as much as possible, and any breaking changes are clearly documented in our release notes.

    Best regards,

    Priyam

Need extra support?

Upgrade your support plan and get personal unlimited phone support with our customer engagement team

Learn More

Forum Channels