Instatic Site Shell Document: Real-Time Collaborative Editing Architecture

Instatic stores the entire website as a single JSON "site-shell" document in the database and uses Yjs CRDT to synchronize edits across multiple users in real time without conflicts.

The CoreBunch/Instatic repository implements a unified approach to content management by treating the whole website as one mutable site-shell document. This document serves as the single source of truth for every page, component, layout, and asset, enabling seamless collaborative editing through a lightweight, self-hosted architecture that runs entirely within a single Bun server.

What Is the Instatic Site Shell Document?

The site-shell is a JSON tree structure—formally defined as a NodeTree—that lives in the sites table under the siteShell column. It describes the complete hierarchy of visual components, their parameters, and slot instances across the entire website.

Because the shell represents the single source of truth, it is:

When editors work in the visual admin interface, they are not modifying individual files or database rows. Instead, they update this shared document, which automatically propagates changes to all connected clients.

The Collaborative Editing Lifecycle

Instatic enables real-time collaboration through a seven-step pipeline that bridges the database, server memory, and connected browsers:

  1. Load: The admin interface fetches the current siteShell JSON via apiRequest and validates it against SiteShellSchema.
  2. Create Y-doc: The client initializes a Y.Doc and loads the JSON into a Y.Map named site.
  3. Connect: A WebSocket opens to /admin/api/cms/site-socket, handled by src/server/collab/siteSocket.ts, which registers the document in the global CollabStore.
  4. Sync: Yjs exchanges binary update messages over the socket, ensuring eventual consistency across all participants.
  5. Apply mutations: Operations like insertNode or updateNodeProps dispatch through src/core/page-tree/mutations.ts, which updates the Y-doc and triggers remote synchronization.
  6. Persist: Saving a draft extracts the current JSON from the Y-doc and writes it back to sites.siteShell inside a transaction (src/server/repositories/siteRepository.ts).
  7. Publish: A separate pipeline bakes static HTML from the committed shell, ensuring the live site only reflects saved states.

Loading and Validating the Site Shell

On initial load, the client retrieves and validates the site-shell document using type-safe API helpers defined in src/core/persistence/validate.ts.

import { apiRequest } from '@core/http';
import { SiteShellSchema } from '@core/schemas/siteShell';

export async function loadSiteShell() {
  const siteShell = await apiRequest('/admin/api/cms/site-shell', {
    schema: SiteShellSchema,
  });
  return siteShell; // typed as the validated JSON shape
}

This validation step ensures that the NodeTree structure conforms to the expected schema before entering the collaborative editing session.

Initializing Yjs for Real-Time Synchronization

Once loaded, the shell enters the collaborative layer through Yjs. The client creates a Y.Doc and connects to the server-side WebSocket endpoint managed by src/server/collab/siteSocket.ts.

import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import { loadSiteShell } from './loadSiteShell';

export async function initCollab() {
  const json = await loadSiteShell();

  // Create a Y-doc and import the JSON tree
  const ydoc = new Y.Doc();
  const yMap = ydoc.getMap('site');
  yMap.set('root', json);

  // Connect to the server-side Y-doc
  const wsProvider = new WebsocketProvider(
    `${location.origin.replace(/^http/, 'ws')}/admin/api/cms/site-socket`,
    'site-doc',
    ydoc,
  );

  // Listen for remote updates
  yMap.observe(() => {
    // React-state update, e.g. via Zustand
  });

  return { ydoc, wsProvider };
}

The WebsocketProvider bridges the client's Y-doc with the server's CollabStore, enabling binary patch streaming that guarantees all participants converge to the same state without manual conflict resolution.

Performing Tree Mutations

All structural changes to the site-shell flow through the mutation engine in src/core/page-tree/mutations.ts. This abstraction ensures that edits are applied consistently to both the local Y-doc and the remote synchronized state.

import { mutateActiveTree } from '@core/page-tree';
import { insertNode } from '@core/page-tree/mutations';

// Example: add a new Text block under the current selected node
function addTextBlock(selectedNodeId: string) {
  mutateActiveTree((tree) => {
    insertNode(tree, {
      parentId: selectedNodeId,
      type: 'base.text',
      props: { content: 'New block' },
    });
  });
}

The mutateActiveTree wrapper handles the Yjs transaction boundaries, ensuring that every mutation automatically propagates to other collaborators as a discrete, reversible update.

Persisting Changes to the Database

When users save drafts, the server extracts the current JSON representation from the Y-doc and persists it transactionally. This process is handled by src/server/repositories/siteRepository.ts, which ensures atomic writes to the sites table.

// In src/server/handlers/cms/saveDraftSite.ts
import { readValidatedBody } from '@core/http';
import { SiteShellSchema } from '@core/schemas/siteShell';
import { siteRepository } from '../repositories/siteRepository';

export async function saveDraftSite(req) {
  const { name, siteShell } = await readValidatedBody(req, {
    type: 'object',
    properties: {
      name: { type: 'string' },
      siteShell: SiteShellSchema,
    },
    required: ['name', 'siteShell'],
  });

  await siteRepository.saveDraft(name, siteShell);
}

This persistence layer separates draft state from published content. The live website is generated only when a publish pipeline bakes static HTML from the committed siteShell, ensuring visitors never see unsaved collaborative edits.

Key Implementation Files

The site-shell architecture and collaborative editing layer span these critical paths:

Summary

  • The site-shell is a single JSON document stored in the sites.siteShell column that serves as the authoritative source of truth for the entire website structure.
  • Yjs CRDT powers real-time collaboration, enabling multiple users to edit simultaneously without locks or manual merges.
  • All mutations route through src/core/page-tree/mutations.ts, ensuring consistent application across local and remote states.
  • The architecture runs entirely within a single Bun server, eliminating external CMS dependencies or hidden caching layers.
  • Drafts persist to the database via siteRepository.ts, while a separate publish pipeline generates static HTML for the live site.

Frequently Asked Questions

How does Instatic handle editing conflicts between multiple users?

Instatic uses Yjs CRDT (Conflict-free Replicated Data Types) to automatically merge concurrent edits. When two users modify different parts of the site-shell simultaneously, Yjs exchanges binary update messages over WebSocket connections that guarantee eventual consistency without requiring manual conflict resolution or locking mechanisms.

Can editors work offline and sync changes later?

While the analysis focuses on real-time synchronization, the Yjs architecture inherently supports offline editing. Because the Y.Doc maintains a complete edit history locally, changes made while disconnected can theoretically be synchronized upon reconnection through the same WebsocketProvider mechanism, though the specific offline policy depends on the client implementation.

What makes the Instatic site-shell different from traditional headless CMS architectures?

Traditional headless CMS platforms often separate content into discrete entries or documents. Instatic treats the entire website as one document, storing it as a unified NodeTree JSON structure. This eliminates cross-reference complexity and enables the visual editor to manipulate the complete site hierarchy—pages, components, layouts, and assets—within a single collaborative session.

How is the site-shell validated for data integrity?

Validation occurs at multiple layers. The HTTP boundary uses TypeBox schemas defined in src/core/persistence/validate.ts to ensure incoming JSON conforms to SiteShellSchema before entering the mutation engine. Additionally, the tree-specific mutations in src/core/page-tree/mutations.ts enforce structural constraints, such as valid parent-child relationships and required slot configurations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →