Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rudra Code

The open-source document engine that lets any product add professional editing, real-time collaboration, and format conversion — embeddable in hours, not months.


What is Rudra Code?

Rudra Code is a modular, pure-Rust SDK for document workflows. It reads, writes, edits, and converts documents across DOCX, ODT, PDF, TXT, Markdown, XLSX, ODS, and CSV formats — with CRDT-based real-time collaboration, a page layout engine, and a production-ready web editor.

CapabilityDescription
Multi-formatDOCX, ODT, PDF, TXT, Markdown, XLSX, ODS, CSV, legacy DOC
Pure RustZero C/C++ dependencies. Compiles to native, WASM, and C FFI
CollaborativeFugue CRDT for conflict-free multi-user editing
Layout enginePagination, text shaping (rustybuzz), font subsetting, PDF export
Web editorProduction-grade browser editor with toolbar, comments, track changes
PDF viewerBuilt-in PDF viewer with annotations (highlight, comment, draw, redact)
SpreadsheetCanvas-based spreadsheet with 60+ formulas, charts, and real-time collab
AI integrationOptional AI sidecar (llama.cpp with Qwen2.5-3B) for document assistance
Self-hostableSingle Docker image with white-labeling support
EmbeddableRust library, WASM module, C shared library, npm packages, or Docker

Architecture

Layer 1: Core Engine (Rust)     — Format I/O, document model, operations, CRDT
Layer 2: Server (Rust/Axum)     — REST API, WebSocket collab, storage, auth, admin
Layer 3: Client SDK (JS/TS)     — Embeddable editor, headless API, React/Vue components
Layer 4: Platform Features      — White-labeling, integration mode, webhooks, AI sidecar

Each layer is independently usable:

  • Just need format conversion? Use Layer 1 as a Rust library or WASM module
  • Need a document API? Use Layers 1+2 — deploy the server via Docker
  • Need an embeddable editor? Use Layers 1+2+3 — npm install and embed
  • Need a full branded product? Use all 4 layers — white-label and self-host

Quick Start

Docker (fastest):

docker run -p 8080:8080 rudra/server
# Editor at http://localhost:8080
# API at http://localhost:8080/api/v1

Rust library:

cargo add s1engine --features full

npm (browser):

npm install @rudra/sdk @rudra/wasm

Tip: See the Quick Start guide for a complete walkthrough, or jump to Docker deployment for self-hosting.

Distribution

I want to…Install
Process documents in Rustcargo add s1engine
Process documents in JS (no UI)npm install @rudra/sdk
Embed an editor in Reactnpm install @rudra/react
Embed an editor in Vuenpm install @rudra/vue
Run a document API serverdocker run rudra/server
Self-host the full platformdocker compose up
Convert DOCX to PDF (CLI)Download from GitHub Releases

Crate Structure

crates/
  s1-model/          Core document model (zero dependencies)
  s1-ops/            Operations, transactions, undo/redo
  s1-format-docx/    DOCX (OOXML) reader/writer
  s1-format-odt/     ODT (ODF) reader/writer
  s1-format-pdf/     PDF export
  s1-format-txt/     Plain text reader/writer
  s1-format-xlsx/    XLSX/ODS/CSV spreadsheet reader/writer
  s1-convert/        Format conversion pipelines
  s1-crdt/           Fugue CRDT for collaboration
  s1-layout/         Page layout, pagination, line breaking
  s1-text/           Text shaping (rustybuzz), font discovery
  s1engine/          Facade crate — high-level public API

License

AGPL-3.0-or-later. Commercial dual-licensing available — contact us for details.

Quick Start

Get up and running with Rudra Code in 5 minutes.

Option 1: Rust Library

cargo add s1engine
#![allow(unused)]
fn main() {
use s1engine::{Engine, DocumentBuilder, Format};

// Open a DOCX file
let engine = Engine::new();
let data = std::fs::read("report.docx")?;
let doc = engine.open(&data)?;
println!("{}", doc.to_plain_text());

// Create a document programmatically
let doc = DocumentBuilder::new()
    .title("My Report")
    .heading(1, "Introduction")
    .text("Built with Rudra Code.")
    .build();

// Export to PDF
let pdf_bytes = doc.export(Format::Pdf)?;
std::fs::write("output.pdf", pdf_bytes)?;
}

Option 2: npm / WASM (Browser)

npm install @rudra/wasm
import init, { WasmEngine } from '@rudra/wasm';

await init();
const engine = new WasmEngine();
const doc = engine.create();
const html = doc.to_html();

Option 3: Docker (Full Editor)

docker run -p 8787:8787 rudra/editor

Open http://localhost:8787 in your browser.

Next Steps

Installation

Rudra Code can be used as a Rust library, WASM npm package, or Docker container.

Prerequisites

PlatformRequirements
RustRust 1.88+ via rustup
WASMNode.js 18+, wasm-pack
DockerDocker 20+

Choose Your Path

Rust Library

Install

[dependencies]
s1engine = "1.0.1"

Open a Document

#![allow(unused)]
fn main() {
use s1engine::Engine;

let engine = Engine::new();
let data = std::fs::read("report.docx")?;
let doc = engine.open(&data)?;

println!("Title: {:?}", doc.metadata().title);
println!("Text: {}", doc.to_plain_text());
println!("Words: {}", doc.to_plain_text().split_whitespace().count());
}

Create a Document

#![allow(unused)]
fn main() {
use s1engine::{DocumentBuilder, Format};

let doc = DocumentBuilder::new()
    .title("My Report")
    .heading(1, "Introduction")
    .text("Built with Rudra Code.")
    .build();

std::fs::write("output.docx", doc.export(Format::Docx)?)?;
std::fs::write("output.pdf", doc.export(Format::Pdf)?)?;
}

Convert Between Formats

#![allow(unused)]
fn main() {
let doc = engine.open(&std::fs::read("input.docx")?)?;
std::fs::write("output.odt", doc.export(Format::Odt)?)?;
}

Feature Flags

FeatureDescriptionDefault
docxDOCX read/writeYes
odtODT read/writeYes
txtPlain textYes
mdMarkdown (GFM)Yes
pdfPDF exportNo
crdtCollaborationNo
convertFormat conversionNo

npm / WASM

Install

npm install @rudra/wasm

Quick Start

import init, { WasmEngine } from '@rudra/wasm';

await init();
const engine = new WasmEngine();

// Create a document
const doc = engine.create();
const html = doc.to_html();

// Open a DOCX file
const response = await fetch('/report.docx');
const bytes = new Uint8Array(await response.arrayBuffer());
const doc2 = engine.open(bytes);
console.log(doc2.to_plain_text());

// Export to PDF
const pdfBytes = doc2.to_pdf();

With Vite

import init, { WasmEngine } from '@rudra/wasm';
const engine = await init().then(() => new WasmEngine());

With Webpack 5

// webpack.config.js
module.exports = { experiments: { asyncWebAssembly: true } };

Bundle Size

  • Release: ~4.1 MB raw, ~1.6 MB gzipped
  • Includes: DOCX, ODT, PDF, TXT, MD support + CRDT + layout engine

Embed in React

Install

npm install @rudra/react @rudra/editor @rudra/wasm

Usage

import { S1EditorComponent } from '@rudra/react';
import { useRef } from 'react';

function MyEditor() {
  const editorRef = useRef(null);

  return (
    <div style={{ height: '100vh' }}>
      <S1EditorComponent
        ref={editorRef}
        theme="default"
        toolbar="standard"
        onReady={() => console.log('Editor ready')}
        onChange={(e) => console.log('Changed:', e.type)}
      />
    </div>
  );
}

Open a File

const handleOpen = async (file: File) => {
  const buffer = await file.arrayBuffer();
  editorRef.current?.open(buffer);
};

Export

const handleExport = async () => {
  const blob = await editorRef.current?.exportDocument('pdf');
  // Download or upload blob
};

With Collaboration

<S1EditorComponent
  collab={{
    serverUrl: 'ws://localhost:8787',
    roomId: 'my-document',
    userName: 'Alice',
  }}
/>

Embed in Vue

Install

npm install @rudra/vue @rudra/editor @rudra/wasm

Usage

<template>
  <S1EditorVue
    ref="editor"
    theme="default"
    toolbar="standard"
    @ready="onReady"
    @change="onChange"
    style="height: 100vh"
  />
</template>

<script setup>
import { ref } from 'vue';
import { S1EditorVue } from '@rudra/vue';

const editor = ref(null);
const onReady = () => console.log('Ready');
const onChange = (e) => console.log('Changed:', e.type);

const openFile = async (file) => {
  const buffer = await file.arrayBuffer();
  editor.value?.open(buffer);
};
</script>

Format Conversion

Rust API

#![allow(unused)]
fn main() {
use s1engine::{Engine, Format};

let engine = Engine::new();
let doc = engine.open(&std::fs::read("input.docx")?)?;

// Convert to any supported format
std::fs::write("output.odt", doc.export(Format::Odt)?)?;
std::fs::write("output.pdf", doc.export(Format::Pdf)?)?;
std::fs::write("output.txt", doc.export(Format::Txt)?)?;
std::fs::write("output.md", doc.export(Format::Md)?)?;
}

Server API

curl -X POST http://localhost:8080/api/v1/convert \
  -F file=@input.docx \
  -F format=pdf \
  -o output.pdf

Supported Conversions

FromDOCXODTPDFTXTMD
DOCX-YesYesYesYes
ODTYes-YesYesYes
TXTYesYesYes-Yes
MDYesYesYesYes-
DOCYesYesYesYesYes

Fidelity Notes

  • DOCX ↔ ODT: Full formatting round-trip
  • Any → PDF: Layout engine with font embedding
  • Any → TXT: Text extraction (formatting lost)
  • DOC → Any: Basic text extraction only

Collaboration Setup

Rudra Code uses Fugue CRDT for conflict-free real-time editing.

Architecture

Client A ──→ WebSocket ──→ Relay Server ──→ WebSocket ──→ Client B
              (operations broadcast to all peers)

Using the Node.js Relay

# Start the relay server
node scripts/relay.js
# Serves editor at http://localhost:8787
# WebSocket at ws://localhost:8787

Using the Rust Server

# Start s1-server (includes WebSocket relay)
cargo run -p s1-server
# REST API at http://localhost:8080/api/v1/
# WebSocket at ws://localhost:8080/ws/collab/{room_id}

Editor Integration

The editor auto-connects when given a relay URL:

// In editor state
state.collabUrl = 'ws://localhost:8787';
state.collabRoom = 'my-document-id';
state.userName = 'Alice';

How It Works

  1. Each client maintains a local CRDT document
  2. Local edits generate operations with unique IDs
  3. Operations are broadcast to all peers via WebSocket
  4. Each peer applies remote operations to their local CRDT
  5. Fugue algorithm guarantees all peers converge to the same state

White-Labeling Rudra Office

This guide covers how to rebrand, restyle, and configure Rudra Office for your own product. Whether you are embedding the editor in a SaaS platform or shipping it as a standalone product under your own name, every visible element can be customized without modifying the editor source code.


Configuration Overview

All runtime configuration is provided through the window.S1_CONFIG object in your index.html. The editor reads this object on startup and adjusts its behavior accordingly. You can either set the object before the editor script loads, or merge your overrides into the default:

<script>
  window.S1_CONFIG = window.S1_CONFIG || {
    apiUrl: '',
    relayUrl: '',
    mode: 'standalone',
    autoCollab: true,
    productName: 'Rudra Office',
    enableCollab: true,
    enablePdfViewer: true,
    enableTemplates: true,
    aiUrl: '',
    enableAI: true,
  };
</script>

Place this block before the editor stylesheet and script tags. Each option is documented in the sections below.


1. Branding

Product Name

Set productName to replace all instances of “Rudra Office” in the UI (title bar, about dialogs, share links):

<script>
  window.S1_CONFIG = {
    ...window.S1_CONFIG,
    productName: 'Acme Docs',
  };
</script>

Update the HTML <title> tag and the .logo-text element to match:

<title>Acme Docs</title>
<div class="logo">
  <img src="/assets/my-logo.svg" alt="Acme" width="28" height="28">
  <span class="logo-text">Acme Docs</span>
</div>

Replace the logo image at /assets/logo.svg with your own SVG or PNG. The default logo slot is 28x28 pixels. If your logo requires different dimensions, adjust the width and height attributes on the <img> element inside the .logo container.

Dark mode note: The editor applies filter: brightness(0) invert(1) to the logo in dark mode so that a dark-on-transparent SVG becomes light. If your logo already handles both themes (e.g., uses currentColor), override this filter:

[data-theme="dark"] .logo img {
  filter: none;
}

Accent Color

The primary brand color is controlled by the --accent CSS custom property. Override it in a <style> block after the editor stylesheet:

<style>
  :root {
    --accent: #6200ea;
    --accent-light: #e8d5ff;
    --accent-bg: #d1b3ff;
  }
</style>

See the Theme System section for the complete list of properties.

Favicon and Manifest

Replace these files in your deployment:

  • /icon-192.svg – favicon / PWA icon
  • /manifest.json – PWA manifest (update name, short_name, icons, theme_color)
  • Update <meta name="theme-color" content="#..."> in index.html

2. Theme System

The editor uses CSS custom properties (variables) defined on :root for all visual elements. Override any of these to change the editor’s appearance without touching the source CSS.

Light Theme Properties (defaults)

PropertyDefaultPurpose
--bg-app#f8f9faApplication background (behind the document canvas)
--bg-white#fffSurface background (panels, cards)
--bg-toolbar#edf2faToolbar and menu bar background
--bg-toolbar-hover#d3e3fdToolbar button hover state
--bg-active#c2dbffActive/pressed toolbar button background
--bg-hover#f1f3f4General hover background for list items
--accent-bg#c2dbffAccent background (selection highlights, badges)
--border#c4c7c5Primary border color
--border-light#dadce0Subtle border color (dividers, separators)
--text-primary#202124Primary text color
--text-secondary#5f6368Secondary text color (labels, descriptions)
--text-muted#80868bMuted text (placeholders, disabled items)
--accent#1a73e8Primary accent color (buttons, links, active states)
--accent-light#d2e3fcLight accent (selected item backgrounds)
--danger#d93025Error / destructive action color
--success#188038Success / confirmation color

Layout Properties

PropertyDefaultPurpose
--radius-sm4pxSmall border radius (buttons, inputs)
--radius-md8pxMedium border radius (cards, panels, modals)
--font-ui'Google Sans', 'Segoe UI', Roboto, ...UI font stack

Elevation (Shadows)

PropertyDefaultPurpose
--shadow-sm0 1px 2px rgba(60,64,67,.3), ...Toolbar, small dropdowns
--shadow-md0 1px 3px rgba(60,64,67,.3), ...Panels, menus
--shadow-lg0 1px 3px rgba(60,64,67,.3), ...Modals, dialogs

Z-Index Hierarchy

PropertyDefaultPurpose
--z-toolbar100Toolbar and menu bar
--z-dropdown200Dropdown menus
--z-sidebar300Side panels (pages, properties, comments)
--z-modal-backdrop400Modal backdrop overlay
--z-modal500Modal dialogs
--z-context-menu600Right-click context menus
--z-toast700Toast notifications

Example: Complete Brand Override

:root {
  --accent: #0052cc;
  --accent-light: #deebff;
  --accent-bg: #b3d4ff;
  --bg-toolbar: #f4f5f7;
  --bg-toolbar-hover: #ebecf0;
  --bg-active: #b3d4ff;
  --radius-sm: 3px;
  --radius-md: 6px;
  --font-ui: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}

3. Dark Mode

The editor supports three dark mode behaviors:

  1. Automatic – Follows the operating system preference via prefers-color-scheme: dark.
  2. Manual toggle – Users click the dark mode button in the status bar, which sets data-theme="dark" on the <html> element.
  3. Explicit override – You can force a theme on page load.

How It Works

  • The [data-theme="dark"] selector overrides all CSS custom properties with dark values.
  • The @media (prefers-color-scheme: dark) block applies the same overrides, gated by :root:not([data-theme="light"]) so that an explicit light preference wins.
  • The user’s choice is persisted in localStorage under the key s1-theme.

Dark Theme Properties

PropertyDark ValueLight Value
--bg-app#202124#f8f9fa
--bg-white#292a2d#fff
--bg-toolbar#35363a#edf2fa
--bg-toolbar-hover#44454a#d3e3fd
--bg-active#1a3a5c#c2dbff
--border#5f6368#c4c7c5
--border-light#3c4043#dadce0
--text-primary#e8eaed#202124
--text-secondary#bdc1c6#5f6368
--text-muted#9aa0a6#80868b
--accent#8ab4f8#1a73e8
--accent-light#1a3a5c#d2e3fc
--danger#f28b82#d93025
--success#81c995#188038

Forcing a Theme Programmatically

// Force dark mode
document.documentElement.setAttribute('data-theme', 'dark');
localStorage.setItem('s1-theme', 'dark');

// Force light mode
document.documentElement.setAttribute('data-theme', 'light');
localStorage.setItem('s1-theme', 'light');

// Reset to OS preference
document.documentElement.removeAttribute('data-theme');
localStorage.removeItem('s1-theme');

Providing Custom Dark Theme Colors

Override the [data-theme="dark"] selector with your own palette:

[data-theme="dark"] {
  --bg-app: #1a1a2e;
  --bg-white: #16213e;
  --bg-toolbar: #1a1a2e;
  --accent: #e94560;
  --accent-light: #3a1528;
}

4. Toolbar Customization

The toolbar is defined in index.html as a series of <button> and <select> elements with specific IDs. You can show or hide individual toolbar items using CSS, or remove them from the HTML entirely.

Toolbar Button Reference

IDFunctionCollapsible Class
fontFamilyFont family dropdowntb-collapsible-768
fontSizeFont size inputtb-collapsible-768
btnBoldBold
btnItalicItalic
btnUnderlineUnderlinetb-collapsible-480
btnStrikeStrikethroughtb-collapsible-1024
btnFormatPainterFormat Paintertb-collapsible-1024
btnSuperscriptSuperscripttb-collapsible-1024
btnSubscriptSubscripttb-collapsible-1024
colorPickerText colortb-collapsible-1024
highlightPickerHighlight colortb-collapsible-1024
btnClearFormatClear formattingtb-collapsible-1024
btnAlignLAlign lefttb-collapsible-480
btnAlignCAlign centertb-collapsible-480
btnAlignRAlign righttb-collapsible-480
btnAlignJJustifytb-collapsible-480
lineSpacingLine spacing dropdowntb-collapsible-1024
btnOutdentDecrease indenttb-collapsible-1024
btnIndentIncrease indenttb-collapsible-1024
btnBulletListBulleted listtb-collapsible-480
btnNumberListNumbered listtb-collapsible-480
btnInsertMenuInsert dropdown menu

Hiding Toolbar Items via CSS

/* Hide the strikethrough button */
#btnStrike { display: none !important; }

/* Hide the format painter */
#btnFormatPainter { display: none !important; }

/* Hide the entire font selection area */
#fontFamily, #fontSize { display: none !important; }

Responsive Collapsing

Toolbar items use tb-collapsible-* classes to automatically hide at specific viewport widths:

  • tb-collapsible-1024 – Hidden below 1024px
  • tb-collapsible-768 – Hidden below 768px
  • tb-collapsible-480 – Hidden below 480px

You can adjust these breakpoints or assign different classes to control which items collapse first.

The menu bar (File, Edit, View, Insert, Format, Review, Tools) can be customized the same way:

/* Hide the Review menu entirely */
[data-menu="reviewMenu"] { display: none !important; }

/* Hide the Tools menu */
[data-menu="toolsMenu"] { display: none !important; }

5. Feature Flags

Feature flags in S1_CONFIG control the availability of major features. When a flag is set to false, the associated UI elements are hidden and the underlying functionality is not initialized.

Available Flags

FlagDefaultEffect
enableCollabtrueEnables the Share button, collaboration status indicator, and real-time editing.
enablePdfViewertrueEnables the built-in PDF viewer tab for opening PDF files.
enableTemplatestrueEnables “New from Template” and “Save as Template” in the File menu.
enableAItrueEnables the AI assistant panel. Requires aiUrl to point to a running LLM endpoint.

Disabling Features

<script>
  window.S1_CONFIG = {
    ...window.S1_CONFIG,
    enableCollab: false,
    enableAI: false,
    enableTemplates: false,
  };
</script>

AI Configuration

The AI assistant connects to a local LLM sidecar (e.g., llama.cpp). Set aiUrl to the base URL of the sidecar and enableAI to true:

<script>
  window.S1_CONFIG = {
    ...window.S1_CONFIG,
    enableAI: true,
    aiUrl: 'http://localhost:8081',
  };
</script>

When enableAI is false or aiUrl is empty, the AI panel and its toolbar entry are not rendered.


6. Custom Fonts

The font family dropdown is defined in index.html as a <select> element with id fontFamily. You can modify the available fonts by editing the <option> list directly.

Default Font List

Arial, Times New Roman, Georgia, Courier New, Verdana,
Trebuchet MS, Garamond, Palatino, Tahoma, Comic Sans MS

Adding Custom Fonts

  1. Load the font via a <link> tag or @font-face rule:
<link href="https://fonts.googleapis.com/css2?family=Lora:wght@400;700&display=swap" rel="stylesheet">
  1. Add the option to the fontFamily select in index.html:
<select id="fontFamily" ...>
  <!-- existing options -->
  <option value="Lora" style="font-family:Lora">Lora</option>
</select>

Replacing the Entire Font List

Remove all existing <option> elements and add your own. Keep the first option as the default:

<select id="fontFamily" ...>
  <option value="">Default</option>
  <option value="Inter" style="font-family:Inter">Inter</option>
  <option value="Source Serif Pro" style="font-family:'Source Serif Pro'">Source Serif Pro</option>
  <option value="Fira Code" style="font-family:'Fira Code'">Fira Code</option>
</select>

Changing the UI Font

The editor UI font is controlled by the --font-ui custom property:

:root {
  --font-ui: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}

This affects all toolbar labels, menus, dialogs, and status bar text. It does not affect the document content font.


7. Locale / i18n

Internationalization support is planned for a future release. The current architecture is prepared for it:

  • All user-facing strings in the toolbar, menus, and dialogs are defined in the HTML and can be replaced.
  • Tooltip text is set via title attributes on every interactive element.
  • Right-to-left (RTL) layout support is available through the BiDi text processing in the engine.

Preparing for i18n Today

If you need to localize the editor before the official i18n module ships, you can:

  1. Replace HTML strings – Edit index.html to translate menu labels, button text, and tooltips.
  2. Override at runtime – Use JavaScript to walk the DOM and replace text content after the editor loads:
document.addEventListener('DOMContentLoaded', () => {
  document.querySelector('#btnBold').title = 'Gras (Ctrl+B)';
  document.querySelector('[data-menu="fileMenu"] .app-menu-btn').textContent = 'Fichier';
  // ... other translations
});
  1. Use a translation map – Maintain a JSON object mapping element IDs/selectors to translated strings, and apply them in a loop.

8. CSS Override Examples

All customizations below should be placed in a <style> block after the editor stylesheet link, or in a separate CSS file loaded after styles.css.

Change the Accent Color to Purple

:root {
  --accent: #7c3aed;
  --accent-light: #ede9fe;
  --accent-bg: #c4b5fd;
}
[data-theme="dark"] {
  --accent: #a78bfa;
  --accent-light: #2e1a5e;
}

Rounded Toolbar Buttons

.tb-btn {
  border-radius: 8px !important;
}

Hide the Status Bar Word Count

.status-bar .word-count {
  display: none;
}

Custom Scrollbar Styling

/* Webkit browsers */
.canvas::-webkit-scrollbar {
  width: 8px;
}
.canvas::-webkit-scrollbar-track {
  background: var(--bg-app);
}
.canvas::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 4px;
}
.canvas::-webkit-scrollbar-thumb:hover {
  background: var(--text-muted);
}

Compact Toolbar (Reduced Height)

.toolbar {
  padding: 2px 8px !important;
  min-height: 32px !important;
}
.tb-btn {
  width: 28px !important;
  height: 28px !important;
}
.tb-btn .msi {
  font-size: 18px !important;
}

Hide the Ruler

.ruler {
  display: none !important;
}

Full-Width Document (No Page Margins)

.doc-page {
  max-width: 100% !important;
  box-shadow: none !important;
  border: none !important;
}

Custom Modal Styling

.modal {
  border-radius: 12px;
  box-shadow: var(--shadow-lg);
}
.modal-header {
  border-bottom: 1px solid var(--border-light);
}

Hide Specific Menu Items

/* Hide "Export as ODT" from the File menu */
[data-fmt="odt"] { display: none !important; }

/* Hide the Templates menu entries */
#btnTemplate, #btnSaveTemplate { display: none !important; }

/* Hide the collaboration Share button */
#btnShare { display: none !important; }

9. Integration Mode

The mode configuration option controls how the editor behaves within a host application.

Standalone Mode (Default)

window.S1_CONFIG = {
  ...window.S1_CONFIG,
  mode: 'standalone',
};

In standalone mode, the editor runs as a full-page application with its own file management, title bar, and menu system. This is the default for deployments where the editor is the primary application.

Integrated Mode

window.S1_CONFIG = {
  ...window.S1_CONFIG,
  mode: 'integrated',
  apiUrl: 'https://your-app.com/api/v1',
  relayUrl: 'wss://your-app.com/ws/collab',
};

In integrated mode, the editor is designed to be embedded within a larger application. Use this when:

  • The editor is loaded inside an <iframe> within your application.
  • File open/save operations are handled by your host application via the API.
  • Collaboration is managed through your own WebSocket relay.

Embedding in an iframe

<iframe
  src="https://editor.your-app.com?room=doc-123"
  style="width: 100%; height: 100vh; border: none;"
  allow="clipboard-read; clipboard-write"
></iframe>

Configuring the API Endpoint

When apiUrl is set, the editor uses it for document CRUD operations and format conversion:

window.S1_CONFIG = {
  ...window.S1_CONFIG,
  apiUrl: 'https://api.your-app.com/v1',
};

Configuring the Collaboration Relay

When relayUrl is set along with autoCollab: true, the editor automatically connects to the WebSocket relay when a room parameter is present in the URL:

window.S1_CONFIG = {
  ...window.S1_CONFIG,
  relayUrl: 'wss://collab.your-app.com/ws',
  autoCollab: true,
};

10. Removing Branding

For commercial licensees with a white-label agreement, you can fully remove Rudra branding.

Step 1: Replace Visual Assets

FilePurpose
/assets/logo.svgTitle bar logo
/icon-192.svgFavicon and PWA icon
/manifest.jsonPWA name, icons, theme color

Step 2: Update HTML

In index.html, update these elements:

<!-- Page title -->
<title>Your Product Name</title>

<!-- Theme color -->
<meta name="theme-color" content="#your-brand-color">

<!-- Logo container -->
<div class="logo">
  <img src="/assets/your-logo.svg" alt="Your Brand" width="28" height="28">
  <span class="logo-text">Your Product Name</span>
</div>

Step 3: Set the Config

<script>
  window.S1_CONFIG = {
    ...window.S1_CONFIG,
    productName: 'Your Product Name',
  };
</script>

Step 4: Override Brand Colors

:root {
  --accent: #your-brand-color;
  --accent-light: #your-brand-light;
  --accent-bg: #your-brand-bg;
}

Step 5: Remove “Powered By” (if present)

If any “Powered by Rudra” or attribution text is displayed, commercial licensees may remove it per the terms of their license agreement. Search the HTML for any such references and remove or replace them.

Licensing Note

Rudra Office is licensed under AGPL-3.0. White-label redistribution without Rudra branding requires a separate commercial license. Contact the maintainers for licensing terms.

Configuration

This guide covers all configuration options for both the Rudra Office client (browser editor) and the rudra-server backend. Configuration is split into two layers: the client-side window.S1_CONFIG object and server-side environment variables (or the s1.toml file).


Client Configuration

The editor reads its configuration from window.S1_CONFIG at startup. Set this object in a <script> tag before the editor script loads. Any property not provided falls back to the default shown below.

<script>
  window.S1_CONFIG = {
    // Server API URL (REST endpoints for document CRUD, conversion).
    // Empty string means the editor runs in offline/standalone mode.
    apiUrl: '',              // e.g., 'http://localhost:8080/api/v1'

    // WebSocket URL for real-time collaboration.
    // Empty string disables automatic WebSocket connection.
    relayUrl: '',            // e.g., 'ws://localhost:8080/ws/collab'

    // Editor mode:
    //   'standalone' — full local editor, no server dependency
    //   'integrated' — expects a server backend for storage and collab
    mode: 'standalone',

    // Automatically connect to the collaboration room on page load.
    // Requires relayUrl to be set and a ?room= parameter in the URL.
    autoCollab: true,

    // Branding — product name displayed in the title bar and dialogs.
    productName: 'Rudra Office',

    // ── Feature Flags ──────────────────────────────────
    enableCollab: true,      // Show the Share button and collaboration UI
    enablePdfViewer: true,   // Allow opening and viewing PDF files
    enableTemplates: true,   // Show the template gallery on new document
    enableAI: true,          // Enable AI writing assistant panel

    // AI sidecar URL (llama.cpp HTTP server).
    // The AI assistant calls this endpoint for completions.
    aiUrl: '',               // e.g., 'http://localhost:8081'
  };
</script>

Property Reference

PropertyTypeDefaultDescription
apiUrlstring''Base URL for the REST API. When empty, the editor operates in standalone mode with no server calls.
relayUrlstring''WebSocket URL for collaboration. When empty, real-time editing is disabled.
modestring'standalone''standalone' or 'integrated'. Integrated mode enables server-backed file management.
autoCollabbooleantrueConnect to the collaboration room automatically when the page loads.
productNamestring'Rudra Office'Display name used in the title bar, dialogs, and branding.
enableCollabbooleantrueShow or hide collaboration controls (Share button, peer indicators).
enablePdfViewerbooleantrueAllow PDF files to be opened in the viewer.
enableTemplatesbooleantrueShow the template gallery when creating a new document.
enableAIbooleantrueEnable the AI writing assistant sidebar.
aiUrlstring''HTTP endpoint for the AI sidecar (llama.cpp). When empty, AI features are unavailable even if enableAI is true.

Overriding Configuration at Runtime

Because S1_CONFIG is a plain JavaScript object on window, host applications can modify it before or after the editor loads:

// Before editor init (in the embedding page)
window.S1_CONFIG = {
  apiUrl: 'https://docs-api.example.com/api/v1',
  relayUrl: 'wss://docs-api.example.com/ws/collab',
  mode: 'integrated',
  productName: 'Acme Docs',
  enableAI: false,
};

For integration mode, the server typically injects this configuration into the HTML response before serving the editor.


Server Configuration

The rudra-server binary reads configuration from two sources, in priority order:

  1. s1.toml file in the working directory (TOML format)
  2. Environment variables prefixed with S1_ (fallback when no TOML file is found)

Environment variables are the recommended approach for container deployments.

Core Settings

VariableTOML KeyDefaultDescription
S1_PORTport8080HTTP port the server listens on.
S1_STORAGEstoragelocalStorage backend: local, memory, or s3.
S1_DATA_DIRdata_dir./dataDirectory for local storage. Documents are stored as {id}.bin with {id}.meta.json sidecars.
S1_STATIC_DIR./publicDirectory containing the built editor frontend (HTML, JS, CSS, WASM).
RUST_LOGs1_server=info,tower_http=infoLog level filter using the tracing EnvFilter syntax.

Example s1.toml

port = 8080
storage = "local"
data_dir = "/var/lib/rudra/data"
max_upload_size = 67108864  # 64 MB

Authentication

VariableDefaultDescription
S1_AUTH_ENABLEDfalseEnable authentication middleware. When false, all endpoints are public.
S1_JWT_SECRETHMAC-SHA256 secret for validating JWT tokens. Required for integration mode (/edit?token=).
S1_REQUIRE_JWT_EXPfalseReject JWT tokens that lack an exp (expiry) claim. Recommended for production.
S1_ALLOW_ANONYMOUStrueAllow unauthenticated requests (Viewer-level access).

When authentication is enabled, the server supports two credential methods:

  • JWT Bearer tokensAuthorization: Bearer <token>. The token payload must include sub (user ID) and name. Optional claims: role (admin or user), tenant_id, exp.
  • API keysX-API-Key: <key>. Configured via the api_keys array in the auth config.

Admin Panel

VariableDefaultDescription
S1_ADMIN_USERAdmin panel username. Both S1_ADMIN_USER and S1_ADMIN_PASS must be set to enable the admin panel.
S1_ADMIN_PASSAdmin panel password.

The admin panel is served at /admin/dashboard and provides real-time monitoring of active sessions, connected editors, memory usage, and server configuration. Sessions are protected by a cookie with a 1-hour timeout. Login attempts are rate-limited to 5 per IP per 60 seconds.

Integration Mode

Integration mode allows embedding Rudra Office in another product. The host application generates a signed JWT and redirects users to /edit?token=<jwt>.

VariableDefaultDescription
S1_JWT_SECRETShared secret between the host application and rudra-server for signing/validating integration JWTs.
S1_CALLBACK_SECRETHMAC-SHA256 key for signing callback request bodies. When set, the server includes an X-S1-Signature: sha256=<hex> header on all callback POST requests.

Integration JWT Claims

ClaimRequiredDescription
file_idYesUnique identifier for the document.
user_idYesIdentifier of the editing user.
user_nameYesDisplay name shown in the collaboration UI.
permissionsNoAccess level: edit (default), comment, or view.
download_urlNoURL from which the server fetches the document on first open. Subject to SSRF protection.
callback_urlNoURL the server POSTs the final document to when the editing session closes.
expNoToken expiry (Unix timestamp). Required if S1_REQUIRE_JWT_EXP=true.

Collaboration (WebSocket)

WebSocket endpoints are available at two URL patterns:

  • /ws/edit/{file_id}?user=NAME&uid=ID&mode=edit
  • /ws/collab/{file_id}?user=NAME&uid=ID&mode=edit
ParameterDefaultDescription
userRandom User-XXXXXXXXDisplay name for presence.
uidRandom UUID (8 chars)Unique peer identifier for this connection.
modeeditEditing mode for this peer.
accesseditAccess level: edit or view. View-only peers cannot send structural operations.

The server manages collaboration rooms automatically:

  • Rooms are created on first connection and destroyed when the last peer leaves.
  • Dirty rooms (with unsaved operations) are flushed to storage every 30 seconds.
  • Operation logs are capped at 10,000 entries per room, with oldest entries truncated when the limit is reached.
  • A ping is sent every 15 seconds to detect dead connections.

Storage Backends

Local Filesystem (default)

S1_STORAGE=local
S1_DATA_DIR=/data

Documents are stored as {id}.bin files with {id}.meta.json metadata sidecars. The directory is created automatically if it does not exist.

In-Memory

S1_STORAGE=memory

All data is held in memory and lost on restart. Suitable for development, testing, and stateless CI environments.

S3-Compatible (planned)

S1_STORAGE=s3
S1_S3_BUCKET=rudra-docs
S1_S3_REGION=us-east-1
S1_S3_ENDPOINT=https://s3.amazonaws.com

S3 storage uses the same StorageBackend trait interface. Any S3-compatible service (AWS S3, MinIO, DigitalOcean Spaces) can be used.


Feature Flags

Feature flags control which UI components are visible and which server capabilities are active.

Client-Side Flags

FlagEffect When Disabled
enableCollabHides the Share button, peer count indicator, and collaboration status bar. WebSocket connections are not initiated.
enablePdfViewerPDF files cannot be opened. The editor shows an “unsupported format” message instead.
enableTemplatesThe template gallery is hidden. New documents start with a blank page.
enableAIThe AI assistant panel and toolbar button are hidden. No requests are made to the AI sidecar.

Server-Side Flags

VariableEffect
S1_AUTH_ENABLED=trueEnforces JWT/API key authentication on all API endpoints.
S1_REQUIRE_JWT_EXP=trueRejects tokens without an exp claim, preventing indefinitely-valid tokens.
S1_ALLOW_ANONYMOUS=falseReturns 401 Unauthorized for requests without credentials.

URL Parameters

The editor reads several URL parameters to control its behavior on load.

ParameterExampleDescription
file?file=abc-123Open an existing file session by ID. The editor fetches the document from the server and connects to the collaboration room.
type?type=sheetHint the editor to open in spreadsheet mode (for multi-app routing). Values: doc, sheet, slide.
room?room=my-roomJoin a specific collaboration room. Used with autoCollab: true.
mode?mode=viewSet the editor to a specific access mode: edit, comment, or view.
token/edit?token=JWTIntegration mode entry point. The server validates the JWT and creates a file session.

Integration Callbacks

When a document editing session closes (all editors leave and the TTL expires, or an explicit save is triggered), the server sends the final document bytes to the configured callback URL.

Callback Flow

  1. Host application creates a file session via /edit?token=JWT with a callback_url in the JWT claims.
  2. Users edit the document collaboratively.
  3. When the session ends, the server POSTs the final document bytes to the callback URL.

Callback Request Headers

HeaderDescription
Content-Typeapplication/octet-stream
X-S1-File-IdThe file session ID.
X-S1-EventEvent type: session.closed (automatic) or document.saved (manual trigger via POST /api/v1/files/{id}/save).
X-S1-TimestampUnix timestamp of the callback.
X-S1-Signaturesha256=<hex> HMAC signature of the request body. Only present when S1_CALLBACK_SECRET is configured.

Manual Save Trigger

To trigger a callback before the session expires:

curl -X POST http://localhost:8080/api/v1/files/{file_id}/save

SSRF Protection

All callback URLs and download_url values are checked against a blocklist before any outbound request is made. The following address ranges are blocked:

  • Loopback: 127.0.0.0/8, ::1
  • Private (RFC 1918): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
  • Link-local: 169.254.0.0/16, fe80::/10
  • Cloud metadata endpoints: 169.254.169.254
  • Hostnames: localhost, *.local, *.internal

Signature Verification

When S1_CALLBACK_SECRET is set, verify the callback authenticity on the receiving end:

import hmac, hashlib

def verify_signature(body: bytes, signature_header: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    received = signature_header.removeprefix("sha256=")
    return hmac.compare_digest(expected, received)

Webhooks

The server supports registering webhook endpoints that receive notifications for document lifecycle events.

Supported Events

EventTrigger
document.createdA new document is uploaded or created.
document.deletedA document is deleted from storage.
document.exportedA document is converted/exported to another format.
*Wildcard: matches all events.

Registration

curl -X POST http://localhost:8080/api/v1/webhooks \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/hook", "events": ["document.created"]}'

Webhook delivery is asynchronous and fire-and-forget. Failed deliveries are logged but not retried.


Complete Production Example

Below is a complete environment configuration suitable for a production deployment:

# Core
S1_PORT=8080
S1_STORAGE=local
S1_DATA_DIR=/data
S1_STATIC_DIR=/app/public

# Authentication
S1_AUTH_ENABLED=true
S1_JWT_SECRET=your-256-bit-secret-here
S1_REQUIRE_JWT_EXP=true
S1_ALLOW_ANONYMOUS=false

# Admin panel
S1_ADMIN_USER=admin
S1_ADMIN_PASS=a-strong-password-here

# Integration callbacks
S1_CALLBACK_SECRET=your-callback-signing-secret

# Logging
RUST_LOG=s1_server=info,tower_http=warn

The corresponding client configuration, injected by the server or embedding page:

window.S1_CONFIG = {
  apiUrl: '/api/v1',
  relayUrl: 'wss://docs.example.com/ws/collab',
  mode: 'integrated',
  productName: 'Example Docs',
  enableCollab: true,
  enableAI: false,
};

Docker Deployment

This guide covers deploying Rudra Office using Docker, from a quick one-liner to a full production stack with persistent storage, collaboration, and an AI writing assistant.


Quick Start

Run the all-in-one image to get a working editor in under a minute:

docker run -d --name rudra -p 8080:8080 rudra/server:latest

Open http://localhost:8080 in your browser. The editor, REST API, and WebSocket collaboration server are all served from a single container on a single port.

EndpointURL
Editor UIhttp://localhost:8080/
REST APIhttp://localhost:8080/api/v1/
WebSocket collabws://localhost:8080/ws/edit/{file_id}
Admin panelhttp://localhost:8080/admin/dashboard
Health checkhttp://localhost:8080/health

To persist documents across container restarts, mount a data volume:

docker run -d --name rudra \
  -p 8080:8080 \
  -v rudra-data:/data \
  rudra/server:latest

Docker Images

Rudra Office publishes three image variants to Docker Hub.

rudra/server (Unified)

The recommended image for most deployments. A single Rust binary that serves the editor frontend, REST API, and WebSocket collaboration – all on one port.

docker run -p 8080:8080 rudra/server
  • Base: debian:bookworm-slim
  • Size: ~40 MB (stripped Rust binary + static assets)
  • Ports: 8080

rudra/editor (Static Frontend Only)

A lightweight image that serves only the editor UI as static files via nginx. Use this when you run the API server separately or want to serve the frontend from a CDN.

docker run -p 80:80 rudra/editor
  • Base: nginx:alpine
  • Size: ~15 MB
  • Ports: 80

rudra/all-in-one (Server + AI Sidecar)

Includes the unified server and an AI writing assistant powered by a local LLM (Qwen2.5-3B via llama.cpp). Suitable for air-gapped environments where cloud AI APIs are not available.

docker compose -f docker-compose.yml up -d
  • AI model: Qwen2.5-3B-Instruct (Q4_K_M quantization, ~2 GB)
  • Minimum RAM: 6 GB (4 GB for AI, 2 GB for server)

Image Tags

All image variants follow the same tagging convention:

TagDescriptionExample
latestLatest stable releaserudra/server:latest
X.Y.ZSpecific version (immutable)rudra/server:1.0.2
X.YLatest patch within a minor releaserudra/server:1.0
XLatest minor within a major releaserudra/server:1
edgeLatest commit on main (unstable)rudra/server:edge

For production deployments, always pin to a specific X.Y.Z tag.


Docker Compose

Minimal (Single Container)

For development and small teams. All state is stored on the local filesystem.

services:
  rudra:
    image: rudra/server:1.0.2
    ports:
      - "8080:8080"
    environment:
      - S1_STORAGE=local
      - S1_DATA_DIR=/data
      - S1_ADMIN_USER=admin
      - S1_ADMIN_PASS=${ADMIN_PASSWORD:-changeme}
    volumes:
      - rudra-data:/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s

volumes:
  rudra-data:

Production (Full Stack)

A production-grade deployment with PostgreSQL for document metadata, MinIO for S3-compatible document storage, Redis for multi-instance collaboration routing, and an optional AI sidecar.

services:
  # ── Rudra Server ─────────────────────────────────────
  rudra-server:
    image: rudra/server:1.0.2
    ports:
      - "${S1_PORT:-8080}:8080"
    environment:
      S1_PORT: "8080"
      S1_STORAGE: "s3"
      S1_DATA_DIR: "/data"
      S1_STATIC_DIR: "/app/public"
      S1_STORAGE_POSTGRES_URL: "postgresql://rudra:${POSTGRES_PASSWORD}@postgres:5432/rudra"
      S1_STORAGE_S3_ENDPOINT: "http://minio:9000"
      S1_STORAGE_S3_BUCKET: "rudra-documents"
      S1_STORAGE_S3_ACCESS_KEY: "${MINIO_ROOT_USER:-minioadmin}"
      S1_STORAGE_S3_SECRET_KEY: "${MINIO_ROOT_PASSWORD:-minioadmin}"
      S1_AUTH_ENABLED: "true"
      S1_JWT_SECRET: "${JWT_SECRET}"
      S1_REQUIRE_JWT_EXP: "true"
      S1_CALLBACK_SECRET: "${CALLBACK_SECRET}"
      S1_ALLOW_ANONYMOUS: "false"
      S1_COLLAB_ENABLED: "true"
      S1_COLLAB_REDIS_URL: "redis://redis:6379"
      S1_ADMIN_USER: "${ADMIN_USER:-admin}"
      S1_ADMIN_PASS: "${ADMIN_PASSWORD}"
      RUST_LOG: "s1_server=info,tower_http=info"
    volumes:
      - rudra-data:/data
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
      minio:
        condition: service_started
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "2"

  # ── PostgreSQL ───────────────────────────────────────
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: rudra
      POSTGRES_USER: rudra
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U rudra"]
      interval: 10s
      timeout: 5s
      retries: 5

  # ── Redis ────────────────────────────────────────────
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
    volumes:
      - redis-data:/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  # ── MinIO (S3-compatible storage) ────────────────────
  minio:
    image: minio/minio:latest
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
    ports:
      - "9001:9001"   # MinIO Console (optional, remove in production)
    volumes:
      - minio-data:/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 30s
      timeout: 5s
      retries: 3

  # ── AI Sidecar (optional) ────────────────────────────
  rudra-ai:
    image: rudra/ai:latest
    build:
      context: ./ai
      dockerfile: Dockerfile
    ports:
      - "${AI_PORT:-8081}:8081"
    deploy:
      resources:
        limits:
          cpus: "${AI_CPUS:-4}"
          memory: "${AI_MEMORY:-4G}"
        reservations:
          memory: "2G"
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-sf", "http://localhost:8081/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

volumes:
  rudra-data:
  pgdata:
  redis-data:
  minio-data:

Create a .env file alongside docker-compose.yml:

# .env
JWT_SECRET=your-secret-key-min-32-chars-long-here
CALLBACK_SECRET=your-callback-signing-secret
ADMIN_USER=admin
ADMIN_PASSWORD=a-strong-admin-password
POSTGRES_PASSWORD=a-strong-postgres-password
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=a-strong-minio-password

Start the stack:

docker compose up -d

Environment Variables

Server Configuration

VariableDefaultDescription
S1_PORT8080TCP port the server listens on.
S1_STORAGElocalStorage backend. One of local, memory, or s3.
S1_DATA_DIR/dataDirectory for local file storage. Only used when S1_STORAGE=local.
S1_STATIC_DIR/app/publicDirectory containing the editor static files (HTML, JS, CSS, WASM).
RUST_LOGs1_server=infoLog level filter. Uses tracing-subscriber EnvFilter syntax.

Storage Backends

VariableDefaultDescription
S1_STORAGE_POSTGRES_URLPostgreSQL connection string for document metadata. Example: postgresql://user:pass@host:5432/dbname.
S1_STORAGE_S3_ENDPOINTS3-compatible endpoint URL. Example: http://minio:9000 or https://s3.amazonaws.com.
S1_STORAGE_S3_BUCKETS3 bucket name for document storage.
S1_STORAGE_S3_ACCESS_KEYS3 access key ID.
S1_STORAGE_S3_SECRET_KEYS3 secret access key.
S1_STORAGE_S3_REGIONus-east-1S3 region (required by some providers).

Authentication

VariableDefaultDescription
S1_AUTH_ENABLEDfalseEnable JWT and API key authentication. When false, all endpoints are public.
S1_JWT_SECRETHMAC-SHA256 secret for validating JWT tokens. Must be set when S1_AUTH_ENABLED=true. Also used for integration mode (/edit?token=).
S1_REQUIRE_JWT_EXPfalseReject JWT tokens that lack an exp (expiry) claim. Recommended for production.
S1_ALLOW_ANONYMOUStrueAllow unauthenticated requests with read-only (Viewer) access. Set to false in production.
S1_CALLBACK_SECRETHMAC-SHA256 key for signing callback request bodies. When set, outgoing callbacks include an X-S1-Signature: sha256=... header for verification.

Collaboration

VariableDefaultDescription
S1_COLLAB_ENABLEDtrueEnable real-time collaborative editing via WebSocket.
S1_COLLAB_REDIS_URLRedis URL for cross-instance collab room routing. Required when running multiple server instances.

Admin Panel

VariableDefaultDescription
S1_ADMIN_USERadminUsername for the admin panel at /admin/dashboard.
S1_ADMIN_PASSadminPassword for the admin panel. Change this in production.

AI Sidecar

VariableDefaultDescription
AI_PORT8081Port for the AI sidecar HTTP API.
AI_CPUS4CPU core limit for the AI container.
AI_MEMORY4GMemory limit for the AI container.

Volumes and Data Persistence

The following volumes should be persisted to avoid data loss:

VolumeContainer PathPurpose
rudra-data/dataDocument files and metadata (when S1_STORAGE=local).
pgdata/var/lib/postgresql/dataPostgreSQL database.
redis-data/dataRedis AOF persistence (collaboration state).
minio-data/dataMinIO object storage (when S1_STORAGE=s3 with MinIO).

Back up the rudra-data and pgdata volumes regularly. For S3-backed deployments, documents are stored in the S3 bucket and only metadata lives in PostgreSQL.

Backup Example

# Stop the stack gracefully
docker compose stop

# Back up PostgreSQL
docker run --rm -v pgdata:/data -v $(pwd):/backup \
  alpine tar czf /backup/pgdata-backup.tar.gz -C /data .

# Back up local document storage
docker run --rm -v rudra-data:/data -v $(pwd):/backup \
  alpine tar czf /backup/rudra-data-backup.tar.gz -C /data .

# Restart
docker compose start

Health Checks

The server exposes a health endpoint at /health:

curl http://localhost:8080/health

Response:

{
  "status": "ok",
  "version": "1.0.2"
}

The admin panel provides a more detailed health endpoint at /admin/api/health (requires admin authentication):

{
  "status": "ok",
  "uptime_secs": 86400,
  "memory_mb": 48.3,
  "active_sessions": 5,
  "active_rooms": 3,
  "total_editors": 12,
  "pid": 1
}

Docker Compose health checks are configured to poll /health every 30 seconds with a 15-second start period, 5-second timeout, and 3 retries. Dependent services wait for the health check to pass before starting.


Build from Source

Prerequisites

  • Docker 20.10+ with BuildKit enabled
  • At least 4 GB of free disk space (Rust compilation is resource-intensive)
  • At least 4 GB of RAM available to the Docker daemon

The Dockerfile.unified performs a multi-stage build:

  1. Stage 1 (rust-builder): Compiles the s1-server Rust binary and the WASM bindings using wasm-pack.
  2. Stage 2 (web-builder): Installs npm dependencies and builds the editor frontend with Vite.
  3. Stage 3 (runtime): Copies the stripped server binary (~15 MB) and the built static assets into a minimal debian:bookworm-slim image.
docker build -f Dockerfile.unified -t rudra/server:local .
docker run -p 8080:8080 rudra/server:local

Server Only (No Frontend)

If you serve the editor frontend separately (e.g., from a CDN), build just the server:

docker build -f server/Dockerfile -t rudra/server-api:local .
docker run -p 8080:8080 rudra/server-api:local

Build Arguments

ArgumentDefaultDescription
RUST_VERSION1.88Rust toolchain version used in the builder stage.
NODE_VERSION20Node.js version used for the frontend build.

Build Caching

The Dockerfiles are optimized for layer caching. Cargo dependency manifests are copied before source code so that changing a source file does not re-download all crates. On a warm cache, incremental rebuilds take approximately 2-3 minutes instead of 10-15 minutes for a full build.


SSL/TLS with a Reverse Proxy

In production, terminate TLS at a reverse proxy in front of the Rudra container. Do not expose port 8080 directly to the internet.

nginx

server {
    listen 443 ssl http2;
    server_name docs.example.com;

    ssl_certificate     /etc/ssl/certs/docs.example.com.pem;
    ssl_certificate_key /etc/ssl/private/docs.example.com.key;

    # Security headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
    add_header X-Content-Type-Options    nosniff;
    add_header X-Frame-Options           SAMEORIGIN;

    # Max upload size (match S1 server limit)
    client_max_body_size 64M;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # WebSocket upgrade for collaborative editing
    location /ws/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host       $host;
        proxy_set_header X-Real-IP  $remote_addr;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }
}

server {
    listen 80;
    server_name docs.example.com;
    return 301 https://$host$request_uri;
}

Traefik (Docker Labels)

Add these labels to the rudra-server service in docker-compose.yml:

rudra-server:
  image: rudra/server:1.0.2
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.rudra.rule=Host(`docs.example.com`)"
    - "traefik.http.routers.rudra.entrypoints=websecure"
    - "traefik.http.routers.rudra.tls.certresolver=letsencrypt"
    - "traefik.http.services.rudra.loadbalancer.server.port=8080"
    # WebSocket support is automatic in Traefik

Caddy

docs.example.com {
    reverse_proxy rudra-server:8080
}

Caddy automatically provisions and renews TLS certificates from Let’s Encrypt.


Scaling

Horizontal Scaling with Redis

To run multiple server instances behind a load balancer, configure Redis for collaboration room routing. Redis ensures that CRDT operations from any server instance are broadcast to all peers in the same editing room, regardless of which instance they are connected to.

services:
  rudra-1:
    image: rudra/server:1.0.2
    environment:
      S1_COLLAB_ENABLED: "true"
      S1_COLLAB_REDIS_URL: "redis://redis:6379"
      S1_STORAGE: "s3"
      # ... other env vars
    deploy:
      replicas: 3

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes

Load Balancer Requirements

When running multiple instances, the load balancer must support:

  • WebSocket connections – use sticky sessions or connection-based routing so that a WebSocket upgrade request reaches the same backend for the entire session lifetime.
  • Health check forwarding – route health probes to /health on port 8080.

Example for HAProxy:

backend rudra_backend
    balance roundrobin
    option httpchk GET /health
    http-check expect status 200
    # Sticky sessions for WebSocket
    stick-table type string len 64 size 100k expire 30m
    stick on req.cook(s1_session_id)
    server rudra1 rudra-1:8080 check
    server rudra2 rudra-2:8080 check
    server rudra3 rudra-3:8080 check

Autoscaling Guidelines

MetricScale Up WhenScale Down When
CPU usage> 70% for 5 minutes< 30% for 10 minutes
Memory usage> 80%< 40%
Active WebSocket connections> 500 per instance< 100 per instance
Request latency (p95)> 500 ms< 100 ms

Resource Limits

ComponentCPUMemoryDisk
rudra/server1-2 cores256-512 MB1 GB (binary + static files)
PostgreSQL1 core256-512 MBDepends on document count
Redis0.5 core128 MBMinimal (in-memory state)
MinIO1 core512 MBDepends on total document size
AI sidecar4 cores4 GB3 GB (model weights)

Docker Resource Limits

Apply resource constraints in your Compose file to prevent a single container from consuming all host resources:

services:
  rudra-server:
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "2"
        reservations:
          memory: 128M
          cpus: "0.5"

For the AI sidecar, allocate at least 4 GB of memory. The LLM model is loaded entirely into RAM at startup:

  rudra-ai:
    deploy:
      resources:
        limits:
          memory: 4G
          cpus: "4"
        reservations:
          memory: 2G

Memory Sizing

The server uses approximately:

  • Base process: 20-40 MB
  • Per active editing session: 1-5 MB (depending on document size)
  • Per WebSocket connection: ~64 KB

For 100 concurrent editors working on 20 documents, expect roughly 150-250 MB of server memory usage.


Configuration File

As an alternative to environment variables, the server reads a s1.toml configuration file from the working directory at startup:

port = 8080
storage = "local"
data_dir = "/data"
max_upload_size = 67108864  # 64 MB

Mount this file into the container:

docker run -p 8080:8080 \
  -v ./s1.toml:/app/s1.toml \
  -v rudra-data:/data \
  rudra/server:1.0.2

Environment variables take precedence over s1.toml values.


Logging

The server uses structured logging via tracing. Control verbosity with the RUST_LOG environment variable:

# Default: info level for the server and tower-http
RUST_LOG=s1_server=info,tower_http=info

# Debug mode (verbose, includes request/response details)
RUST_LOG=s1_server=debug,tower_http=debug

# Trace mode (extremely verbose, includes CRDT operations)
RUST_LOG=s1_server=trace

# Quiet mode (warnings and errors only)
RUST_LOG=s1_server=warn

Logs are written to stdout in plain text format, suitable for collection by Docker logging drivers, Fluentd, Loki, or any other log aggregator.


Troubleshooting

Container Exits Immediately

Check the logs:

docker logs rudra

Common causes:

  • Port 8080 already in use on the host. Change the host port: -p 9090:8080.
  • The /data volume directory has incorrect permissions.

WebSocket Connections Fail

  • Verify that your reverse proxy passes WebSocket upgrade headers. See the SSL/TLS section for correct nginx and Traefik configuration.
  • Check that the Connection: Upgrade and Upgrade: websocket headers reach the server.

Admin Panel Returns 403

  • S1_ADMIN_USER and S1_ADMIN_PASS must both be set and non-empty.
  • Sessions expire after 1 hour. Log in again.

JWT Authentication Rejected

  • Ensure S1_JWT_SECRET matches the secret used to sign tokens.
  • If S1_REQUIRE_JWT_EXP=true, all tokens must include an exp claim.
  • Check that the token has not expired (exp must be in the future).

High Memory Usage

  • Check the number of active editing sessions in the admin panel (/admin/dashboard).
  • Large documents (> 10 MB) consume proportionally more memory during editing.
  • Set memory limits with deploy.resources.limits.memory in Compose to prevent unbounded growth.

Self-Hosting

This guide covers deploying Rudra Office on your own infrastructure. The server ships as a single Rust binary (or Docker image) that serves the editor UI, REST API, and WebSocket collaboration – no reverse proxy or sidecar process required for basic deployments.


Requirements

Minimum Hardware

ComponentMinimumRecommended
CPU1 vCPU2+ vCPUs
RAM512 MB2 GB
Disk1 GB10 GB+ (depends on document volume)
Network1 Mbps100 Mbps

The server is single-process and multi-threaded (Tokio async runtime). Each active collaboration room holds its operation log in memory. Plan approximately 1 MB of RAM per active room with moderate edit history.

Software

DependencyVersionNotes
Docker20.10+For container deployments. Docker Compose v2 recommended.
LinuxAny modern distroThe container image uses debian:bookworm-slim.
curlAnyUsed by the Docker health check.

For bare-metal deployments without Docker, you need Rust 1.88+ and Node.js 20+ to build from source.


Quick Deploy

Create a docker-compose.yml:

services:
  s1-server:
    image: rudra/server:latest
    ports:
      - "8080:8080"
    environment:
      - S1_PORT=8080
      - S1_STORAGE=local
      - S1_DATA_DIR=/data
      - S1_STATIC_DIR=/app/public
      - S1_ADMIN_USER=admin
      - S1_ADMIN_PASS=changeme
      - RUST_LOG=s1_server=info,tower_http=info
    volumes:
      - s1-data:/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s

volumes:
  s1-data:

Start the server:

docker compose up -d

Open http://localhost:8080 in your browser. The admin panel is at http://localhost:8080/admin/dashboard.

With AI Sidecar

To include the AI writing assistant (CPU-based, no GPU required):

services:
  s1-server:
    image: rudra/server:latest
    ports:
      - "8080:8080"
    environment:
      - S1_PORT=8080
      - S1_STORAGE=local
      - S1_DATA_DIR=/data
      - S1_ADMIN_USER=admin
      - S1_ADMIN_PASS=changeme
    volumes:
      - s1-data:/data
    restart: unless-stopped

  rudra-ai:
    build:
      context: ./ai
      dockerfile: Dockerfile
    ports:
      - "8081:8081"
    deploy:
      resources:
        limits:
          cpus: "4"
          memory: "4G"
        reservations:
          memory: "2G"
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-sf", "http://localhost:8081/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

volumes:
  s1-data:

Set the client configuration to point to the AI sidecar:

window.S1_CONFIG = {
  aiUrl: 'http://localhost:8081',
  enableAI: true,
};

Build from Source

# Clone the repository
git clone https://github.com/example/doc-engine.git
cd doc-engine

# Build the unified image (server + editor + WASM)
docker build -f Dockerfile.unified -t rudra/server .

# Run
docker run -d \
  -p 8080:8080 \
  -v rudra-data:/data \
  -e S1_ADMIN_USER=admin \
  -e S1_ADMIN_PASS=changeme \
  rudra/server

Production Checklist

Before exposing Rudra Office to users, verify the following:

Security

  • Set a strong S1_JWT_SECRET (at least 32 random bytes, base64-encoded).
  • Set S1_REQUIRE_JWT_EXP=true to reject tokens without expiry.
  • Set S1_AUTH_ENABLED=true if the instance is not behind an authenticating proxy.
  • Set S1_ALLOW_ANONYMOUS=false to prevent unauthenticated access.
  • Change S1_ADMIN_USER and S1_ADMIN_PASS from their defaults.
  • Set S1_CALLBACK_SECRET if using integration callbacks, so the host app can verify request authenticity.
  • Terminate TLS at the reverse proxy (or load balancer). Never expose plain HTTP to the internet.

Reliability

  • Use a persistent volume for /data (Docker named volume or host mount).
  • Enable the Docker health check (included in the default Dockerfile).
  • Set restart: unless-stopped (or always) in your Compose file.
  • Configure log rotation (Docker logging driver or external collector).

Performance

  • Set RUST_LOG=s1_server=info,tower_http=warn to reduce log volume.
  • Set appropriate resource limits in Docker (cpus, memory).
  • Monitor the /admin/api/health endpoint for memory and session counts.

Architecture Options

Single Node

The simplest deployment. One container runs the entire stack:

Internet --> [Reverse Proxy (TLS)] --> rudra-server:8080
                                           |
                                           +--> /        (editor UI)
                                           +--> /api/v1/ (REST API)
                                           +--> /ws/     (WebSocket)
                                           +--> /admin/  (admin panel)

Suitable for teams of up to ~50 concurrent editors. The server uses Tokio’s multi-threaded runtime, so it scales well on a single machine with multiple cores.

Multi-Node (Horizontal Scaling)

For larger deployments, run multiple rudra-server instances behind a load balancer with sticky sessions:

Internet --> [Load Balancer (TLS + sticky sessions)]
                 |          |          |
                 v          v          v
             server-1   server-2   server-3
                 |          |          |
                 +----------+----------+
                            |
                     [Shared Storage]
                     (S3 / PostgreSQL)

Requirements for multi-node:

  • Sticky sessions – WebSocket connections must be routed to the same backend for the duration of the editing session. Use cookie-based or IP-based affinity.
  • Shared storage – All nodes must share the same storage backend (S3 or a shared filesystem). In-memory and local storage are not suitable for multi-node.
  • Collaboration rooms are per-node – Each server instance manages its own set of rooms. Editors connected to different nodes for the same document will not see each other’s changes in real time. For full cross-node collaboration, place a shared message broker (Redis Pub/Sub or NATS) in front of the room manager. This is a planned feature.

Reverse Proxy

nginx

upstream rudra {
    server 127.0.0.1:8080;
}

server {
    listen 443 ssl http2;
    server_name docs.example.com;

    ssl_certificate     /etc/ssl/certs/docs.example.com.crt;
    ssl_certificate_key /etc/ssl/private/docs.example.com.key;

    # Security headers
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Max upload size (match S1 max_upload_size)
    client_max_body_size 64m;

    # Proxy all traffic to rudra-server
    location / {
        proxy_pass http://rudra;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # WebSocket upgrade for collaboration
    location /ws/ {
        proxy_pass http://rudra;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # Longer timeouts for WebSocket connections
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

# HTTP to HTTPS redirect
server {
    listen 80;
    server_name docs.example.com;
    return 301 https://$host$request_uri;
}

Traefik

Using Docker labels with Traefik v2:

services:
  s1-server:
    image: rudra/server:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.rudra.rule=Host(`docs.example.com`)"
      - "traefik.http.routers.rudra.tls=true"
      - "traefik.http.routers.rudra.tls.certresolver=letsencrypt"
      - "traefik.http.services.rudra.loadbalancer.server.port=8080"
      # Sticky sessions for WebSocket affinity
      - "traefik.http.services.rudra.loadbalancer.sticky.cookie=true"
      - "traefik.http.services.rudra.loadbalancer.sticky.cookie.name=rudra_backend"
    environment:
      - S1_PORT=8080
      - S1_STORAGE=local
      - S1_DATA_DIR=/data
    volumes:
      - s1-data:/data

Caddy

docs.example.com {
    reverse_proxy localhost:8080

    @websocket {
        header Connection *Upgrade*
        header Upgrade    websocket
    }
    reverse_proxy @websocket localhost:8080
}

Caddy handles TLS automatically via Let’s Encrypt.


Backup and Restore

Local Storage

When using S1_STORAGE=local, documents are stored in the S1_DATA_DIR directory as flat files:

/data/
  {doc-id}.bin           # Document bytes
  {doc-id}.meta.json     # Metadata sidecar (JSON)
  {doc-id}_ops.json      # Collaboration operation log (if room was active)

Backup

# Stop the server to ensure consistency (or accept point-in-time)
docker compose stop s1-server

# Create a compressed archive
tar czf rudra-backup-$(date +%Y%m%d).tar.gz /data/

# Restart
docker compose start s1-server

For zero-downtime backups, copy the data directory while the server is running. The files are append-only from the server’s perspective; individual file reads are atomic.

Restore

docker compose stop s1-server
rm -rf /data/*
tar xzf rudra-backup-20260322.tar.gz -C /
docker compose start s1-server

S3-Compatible Storage

When using S3 storage, backups are handled by the storage provider’s own mechanisms:

# Cross-region replication (AWS)
aws s3 sync s3://rudra-docs s3://rudra-docs-backup --region us-west-2

# MinIO mirror
mc mirror minio/rudra-docs backup/rudra-docs

PostgreSQL (planned)

For deployments using PostgreSQL as the metadata store:

# Dump
pg_dump -h localhost -U rudra -d rudra_docs > rudra-$(date +%Y%m%d).sql

# Restore
psql -h localhost -U rudra -d rudra_docs < rudra-20260322.sql

Schedule automated dumps with cron or use continuous archiving with WAL-G for point-in-time recovery.


Monitoring

Health Endpoints

EndpointAuth RequiredDescription
GET /healthNoBasic health check. Returns {"status":"ok","version":"..."}. Use this for load balancer probes and Docker health checks.
GET /admin/api/healthYes (admin)Detailed health metrics including uptime, memory usage, active sessions, active rooms, total editors, and PID.
GET /admin/api/statsYes (admin)Server statistics: uptime, session count, editor count, memory.
GET /api/v1/infoNoServer version, engine version, and supported format list.

Prometheus Metrics (planned)

A /metrics endpoint exposing Prometheus-compatible metrics is planned. In the meantime, scrape the /admin/api/health JSON endpoint with a custom exporter or use the health check for alerting.

Key Metrics to Monitor

MetricSourceAlert Threshold
HTTP response statusReverse proxy logsError rate > 1%
WebSocket connections/admin/api/stats (total_editors)Approaching connection limit
Memory usage/admin/api/health (memory_mb)> 80% of container limit
Active sessions/admin/api/stats (active_sessions)Unexpected drop to 0
Uptime/admin/api/health (uptime_secs)Reset indicates crash/restart
Disk usageHost monitoring> 85% of volume capacity

Logging

The server uses the tracing crate with structured logging. Control verbosity with the RUST_LOG environment variable:

# Production (recommended)
RUST_LOG=s1_server=info,tower_http=warn

# Debugging collaboration issues
RUST_LOG=s1_server=debug,tower_http=info

# Trace-level for development
RUST_LOG=s1_server=trace,tower_http=trace

Logs are written to stdout in plain text format. Use your container runtime’s log driver to forward them to a log aggregator (Loki, Elasticsearch, CloudWatch).

Client Error Reporting

The editor sends client-side errors to POST /api/v1/errors. These are stored in an in-memory ring buffer (capacity: 100) and visible in the admin panel at /admin/api/errors:

curl -s http://localhost:8080/admin/api/errors?limit=10 | jq .

Upgrades

Rolling Updates (Docker Compose)

# Pull the new image
docker compose pull s1-server

# Recreate with zero downtime (if using restart: always)
docker compose up -d --no-deps s1-server

Active WebSocket connections will be dropped during the restart. Clients reconnect automatically (the editor has built-in reconnection logic with exponential backoff).

Pre-Upgrade Checklist

  1. Back up data – Create a snapshot of the data volume before upgrading.
  2. Check release notes – Look for breaking changes in configuration or storage format.
  3. Test in staging – Run the new version against a copy of your data before deploying to production.

Database Migrations (planned)

When PostgreSQL support is added, migrations will be handled automatically on server startup using an embedded migration runner. The server will refuse to start if it detects a schema version newer than what it supports (preventing accidental downgrades).

Rollback

If an upgrade causes issues:

# Roll back to the previous image
docker compose down
docker compose up -d --pull never s1-server

If the data format has changed, restore from the backup taken before the upgrade.


Security Hardening

CORS

The server ships with a permissive CORS policy (CorsLayer::permissive()) for development convenience. In production, configure your reverse proxy to enforce a strict CORS policy:

# nginx — restrict to your domain
add_header Access-Control-Allow-Origin "https://docs.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-API-Key" always;
add_header Access-Control-Max-Age 86400 always;

Rate Limiting

The admin login endpoint has built-in rate limiting (5 attempts per IP per 60 seconds). For API endpoints, add rate limiting at the reverse proxy layer:

# nginx — rate limit API requests
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;

location /api/ {
    limit_req zone=api burst=50 nodelay;
    proxy_pass http://rudra;
}

SSRF Protection

The server blocks outbound HTTP requests to private and internal IP addresses. This protects against Server-Side Request Forgery when processing download_url and callback_url values from integration JWTs. The following are blocked:

  • RFC 1918 private ranges
  • Loopback addresses
  • Link-local addresses
  • Cloud metadata endpoints (169.254.169.254)
  • Hostnames ending in .local or .internal

No additional configuration is needed; SSRF protection is always active.

Content Security Policy

Add a strict CSP header at your reverse proxy:

add_header Content-Security-Policy "
    default-src 'self';
    script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
    style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
    font-src 'self' https://fonts.gstatic.com;
    img-src 'self' data: blob:;
    connect-src 'self' wss://docs.example.com;
" always;

Adjust the connect-src directive to match your domain and any AI sidecar URLs.

File Upload Validation

The server validates all uploaded documents through the s1engine parser before accepting them. Malformed or unrecognized files are rejected with a 400 Bad Request. The maximum upload size is 64 MB by default (configurable via max_upload_size in s1.toml).

Network Isolation

In a Docker Compose deployment, isolate internal services from the public network:

services:
  s1-server:
    networks:
      - frontend
      - backend

  rudra-ai:
    networks:
      - backend  # AI sidecar not exposed to internet

networks:
  frontend:
  backend:
    internal: true

Troubleshooting

Common Issues

Editor loads but shows “Failed to load WASM”

The static directory (S1_STATIC_DIR) does not contain the compiled WASM files. Verify the build completed successfully and that the public/ directory contains .wasm files.

WebSocket connections fail with 403

If S1_AUTH_ENABLED=true, WebSocket connections may require authentication. Ensure the client passes credentials (JWT or API key) in the WebSocket URL query parameters or upgrade headers.

“S1_JWT_SECRET not set” warning on startup

Integration mode (/edit?token=) requires a JWT secret. Set S1_JWT_SECRET to a strong random string. This warning is informational if you are not using integration mode.

Admin panel returns 403

Both S1_ADMIN_USER and S1_ADMIN_PASS must be set as environment variables. If either is empty, the admin panel is disabled entirely.

High memory usage

Check the number of active rooms via /admin/api/stats. Each room with a large operation log consumes memory. The server truncates logs at 10,000 entries, but many concurrent rooms can still accumulate. Consider restarting stale sessions via the admin panel.

Rust API Reference

Full API documentation is auto-generated from source: cargo doc --open

Core Types

Engine

#![allow(unused)]
fn main() {
let engine = Engine::new();
let doc = engine.open(&bytes)?;       // Open from bytes
let doc = engine.open_file("path")?;  // Open from file
let doc = engine.create();             // New empty document
}

Document

#![allow(unused)]
fn main() {
doc.to_plain_text()           // Extract text
doc.to_html()                 // Render to HTML
doc.export(Format::Pdf)?      // Export to format
doc.metadata()                // Get metadata
doc.model()                   // Access document model
doc.apply(operation)?         // Apply an operation
doc.undo()?                   // Undo last operation
doc.redo()?                   // Redo
}

DocumentBuilder

#![allow(unused)]
fn main() {
DocumentBuilder::new()
    .title("Report")
    .heading(1, "Chapter 1")
    .text("Content here.")
    .bold("Important!")
    .table(|t| t.row(|r| r.cell("A").cell("B")))
    .build()
}

Format

#![allow(unused)]
fn main() {
enum Format { Docx, Odt, Pdf, Txt, Md, Doc }
}

Crate Documentation

Each crate has its own rustdoc:

cargo doc -p s1-model --open
cargo doc -p s1-ops --open
cargo doc -p s1-format-docx --open
cargo doc -p s1engine --open

WASM / JavaScript API

WasmEngine

import init, { WasmEngine } from '@rudra/wasm';
await init();

const engine = new WasmEngine();
const doc = engine.create();
const doc2 = engine.open(uint8Array);

WasmDocument

Content

doc.to_html()              // Full HTML rendering
doc.to_plain_text()        // Text extraction
doc.to_paginated_html()    // Paginated HTML with layout

Export

doc.to_docx()   // Uint8Array
doc.to_odt()    // Uint8Array
doc.to_pdf()    // Uint8Array
doc.export('md') // Uint8Array

Editing

doc.insert_text_in_paragraph(nodeId, offset, "text")
doc.delete_text_in_paragraph(nodeId, offset, length)
doc.split_paragraph(nodeId, offset)
doc.format_selection(startNode, startOff, endNode, endOff, key, value)
doc.set_heading_level(nodeId, level)
doc.insert_table(afterNodeId, rows, cols)
doc.insert_image(afterNodeId, bytes, mimeType)

Undo/Redo

doc.undo()       // Returns boolean
doc.redo()       // Returns boolean
doc.can_undo()   // Boolean
doc.can_redo()   // Boolean

Metadata

doc.set_title("My Doc")
doc.set_author("Alice")
doc.get_document_stats_json() // { words, characters, paragraphs, pages }

C FFI API

Rudra Code provides C-compatible bindings via opaque handles.

Build

cd ffi/c
cargo build --release
# Output: target/release/libs1engine_c.{so,dylib,dll}

API

#include "s1engine.h"

// Create engine
S1Engine* engine = s1_engine_new();

// Open a document
S1Error* err = NULL;
S1Document* doc = s1_engine_open(engine, data, len, &err);

// Get text
S1String* text = s1_document_plain_text(doc);
printf("%s\n", s1_string_ptr(text));

// Export
S1Bytes* docx = s1_document_export(doc, "docx", &err);
// Use s1_bytes_data() and s1_bytes_len()

// Cleanup
s1_string_free(text);
s1_bytes_free(docx);
s1_document_free(doc);
s1_engine_free(engine);

Handle Types

TypeDescriptionFree Function
S1EngineEngine instances1_engine_free
S1DocumentOpen documents1_document_free
S1ErrorError messages1_error_free
S1StringUTF-8 strings1_string_free
S1BytesByte buffers1_bytes_free

REST API

The rudra-server provides a REST API for document management.

Base URL

http://localhost:8080/api/v1/

Endpoints

Documents

MethodEndpointDescription
POST/documentsUpload a document (multipart)
GET/documentsList documents (paginated)
GET/documents/:idGet document metadata
GET/documents/:id/contentDownload document bytes
DELETE/documents/:idDelete a document
GET/documents/:id/thumbnailGet PDF thumbnail

Conversion

MethodEndpointDescription
POST/convertStateless format conversion

Webhooks

MethodEndpointDescription
POST/webhooksRegister a webhook
GET/webhooksList webhooks
DELETE/webhooks/:idDelete a webhook

System

MethodEndpointDescription
GET/healthHealth check
GET/infoServer info

Example: Upload and Convert

# Upload a DOCX
curl -X POST http://localhost:8080/api/v1/documents \
  -F file=@report.docx

# Convert to PDF
curl -X POST http://localhost:8080/api/v1/convert \
  -F file=@report.docx \
  -F format=pdf \
  -o report.pdf

WebSocket Protocol

Real-time collaboration uses WebSocket at:

ws://localhost:8080/ws/collab/{room_id}

Connection Flow

  1. Client connects to ws://host/ws/collab/my-doc-123
  2. Server sends welcome: {"type":"welcome","roomId":"my-doc-123","opsCount":0}
  3. If late joiner, server sends catch-up ops: {"type":"catchUp","op":{...}}
  4. Client sends operations as JSON text messages
  5. Server validates and broadcasts to all peers in the room

Message Types

Client → Server

{"type":"op","action":"insertText","nodeId":"0:5","offset":3,"text":"hello"}
{"type":"op","action":"formatSelection","startNode":"0:5","startOffset":0,"endNode":"0:5","endOffset":5,"key":"bold","value":"true"}
{"type":"awareness","cursor":{"nodeId":"0:5","offset":3},"user":"Alice"}

Server → Client

{"type":"welcome","roomId":"my-doc","opsCount":42}
{"type":"catchUp","op":{"action":"insertText",...}}

Room Lifecycle

  • Room created on first peer join
  • All operations broadcast to all peers
  • Operations logged for late-joiner catch-up
  • Dirty rooms auto-saved to storage every 30 seconds
  • Room closed when last peer disconnects

Architecture Overview

Consumer Applications
        |  Rust API / C FFI / WASM
+-------v--------------------------------------------+
|                s1engine (facade)                    |
|----------------------------------------------------|
|  s1-ops       s1-layout       s1-convert           |
|  Operations   Page Layout     Format Conversion    |
|  Undo/Redo    Pagination      DOC → DOCX           |
|----------------------------------------------------|
|  s1-crdt                s1-model                   |
|  Collaborative          Core Document Model        |
|  Editing (Fugue)        (zero external deps)       |
|----------------------------------------------------|
|  format-docx  format-odt  format-pdf  format-txt   |
|  format-md                                         |
|----------------------------------------------------|
|                s1-text (Pure Rust)                  |
|        rustybuzz  ttf-parser  fontdb               |
+----------------------------------------------------+

Design Principles

  1. s1-model has zero external dependencies — pure Rust data structures
  2. All mutations via Operations — never modify the tree directly
  3. Format isolation — each format crate only depends on s1-model
  4. No panics in library code — all public functions return Result
  5. No unsafe — unless documented with a SAFETY comment

Document Model

The document model (s1-model) is a tree of nodes:

Document
  └─ Body
       ├─ Paragraph
       │    ├─ Run (formatting container)
       │    │    ├─ Text ("Hello ")
       │    │    └─ Text ("world")
       │    └─ Run (bold)
       │         └─ Text ("bold text")
       ├─ Table
       │    ├─ TableRow
       │    │    ├─ TableCell
       │    │    └─ TableCell
       │    └─ TableRow
       └─ Paragraph

Node Types

TypeDescriptionCan Contain
DocumentRoot nodeBody, Header, Footer, CommentBody
BodyMain contentParagraph, Table, TOC, PageBreak
ParagraphText containerRun, Image, LineBreak, Tab, Field
RunFormatting spanText
TextLeaf text content(none)
TableGrid containerTableRow
TableRowRowTableCell
TableCellCellParagraph
ImageInline image(none)

NodeId

Every node has a globally unique NodeId(replica_id, counter):

  • replica_id identifies the creating peer (for CRDT)
  • counter is monotonically increasing per replica

Attributes

Nodes carry an AttributeMap of key-value formatting:

  • Character: Bold, Italic, FontSize, FontFamily, Color
  • Paragraph: Alignment, SpacingBefore, SpacingAfter, IndentLeft
  • Image: ImageWidth, ImageHeight, ImageMediaId
  • Style: StyleId references a named Style definition

Operations & CRDT

All document mutations go through the Operation enum:

#![allow(unused)]
fn main() {
enum Operation {
    InsertNode { parent_id, index, node },
    DeleteNode { target_id },
    MoveNode { target_id, new_parent_id, new_index },
    InsertText { target_id, offset, text },
    DeleteText { target_id, offset, length },
    SetAttributes { target_id, attributes },
    RemoveAttributes { target_id, keys },
    SetMetadata { key, value },
    SetStyle { style },
    RemoveStyle { style_id },
}
}

Undo/Redo

Every operation produces an inverse. Applying the inverse undoes the change:

InsertNode → inverse: DeleteNode (with snapshot)
InsertText → inverse: DeleteText
SetAttributes → inverse: SetAttributes (with previous values)

Transactions

Multiple operations can be grouped into a Transaction for atomic undo:

#![allow(unused)]
fn main() {
let mut txn = Transaction::with_label("Bold selection");
txn.push(Operation::set_attributes(run1, bold_attrs));
txn.push(Operation::set_attributes(run2, bold_attrs));
doc.apply_transaction(&txn)?;
// Single undo reverts both
}

CRDT (Fugue)

The CRDT layer (s1-crdt) uses the Fugue algorithm for text:

  • Each character has a unique ID + origin references
  • Concurrent inserts produce deterministic interleaving
  • State vectors track what each replica has seen
  • Convergence guaranteed regardless of operation order

Format Crates

Each format has its own crate that only depends on s1-model.

DOCX (s1-format-docx)

Reads and writes Office Open XML (ECMA-376):

  • Paragraph/run formatting, styles, lists
  • Tables (including nested), images, hyperlinks
  • Headers/footers, comments, footnotes/endnotes
  • Track changes, bookmarks, section properties
  • Media deduplication by content hash

ODT (s1-format-odt)

Reads and writes Open Document Format (ODF 1.2):

  • Full formatting parity with DOCX
  • Automatic styles → node attributes mapping
  • Table column parsing with repeat support
  • TOC source attributes preserved

PDF (s1-format-pdf)

Export only (via pdf-writer):

  • Font embedding with subsetting
  • Proper ToUnicode CMap for text extraction
  • JPEG passthrough, PNG decode
  • Page layout from s1-layout

TXT & Markdown (s1-format-txt, s1-format-md)

  • Plain text: paragraph-per-line
  • Markdown: GFM tables, headings, lists, images, code blocks

Layout Engine

The layout engine (s1-layout) paginates documents for PDF export and editor display.

Features

  • Knuth-Plass line breaking algorithm
  • Widow/orphan control
  • Text shaping via rustybuzz (pure Rust HarfBuzz)
  • Font loading via fontdb with substitution table
  • Hyphenation (English) via Knuth-Liang patterns
  • BiDi text support via unicode-bidi
  • Table layout with column width distribution
  • Image placement with dimension constraints
  • Margin collapsing (CSS spec-compliant)

Configuration

#![allow(unused)]
fn main() {
LayoutConfig {
    page_width: 612.0,   // US Letter (points)
    page_height: 792.0,
    margin_top: 72.0,    // 1 inch
    margin_bottom: 72.0,
    margin_left: 72.0,
    margin_right: 72.0,
    min_orphan_lines: 2,
    min_widow_lines: 2,
}
}

Performance

  • Full layout of a 10-page document: ~50ms
  • Incremental layout: planned (dirty_from_page field exists)
  • Font cache: LRU with 50K entry fallback cache

Development Setup

Prerequisites

  • Rust 1.88+ (rustup install stable)
  • wasm-pack (cargo install wasm-pack)
  • Node.js 18+ and npm
  • Git

Clone & Build

git clone https://github.com/Rudra-Office/Rudra-Editor.git
cd s1engine

# Build and test all Rust crates
cargo build --workspace
cargo test --workspace

# Build WASM bindings
make wasm

# Start the editor dev server
cd editor && npm install && npm run dev

Verify Setup

make check  # Runs fmt + clippy + all tests

Project Structure

crates/           Rust library crates (model, ops, formats, etc.)
ffi/wasm/         WASM bindings
ffi/c/            C FFI bindings
editor/           Browser editor (vanilla JS)
server/           REST API server (Axum)
packages/         npm packages (SDK, editor, React, Vue, Web Component)
docs/             Internal documentation and trackers
docs-site/        Public documentation (mdBook)
scripts/          Build and utility scripts
examples/         Example projects

Architecture Rules

These are enforced in code review:

1. Document Model is Sacred

s1-model has zero external dependencies. Every node must have a globally unique NodeId(replica_id, counter). Never expose internal model representation in public API.

2. All Mutations Via Operations

Never modify the document tree directly. All changes go through Operation → applied via s1-ops. Every operation must implement invert() for undo.

3. Format Isolation

Each format crate only depends on s1-model. Format crates never depend on each other or on s1-ops/s1-layout.

4. No Panics in Library Code

All public functions return Result<T, Error>. No .unwrap() or .expect() in library code. Tests are fine.

5. No Unsafe

Unless absolutely necessary, with a documented // SAFETY: comment.

6. Editor UI Standards

  • Clean, professional look following production-grade UI standards
  • No emojis in UI
  • Every button needs a title tooltip
  • Use CSS variables, not hardcoded colors

Testing

Run Tests

# All tests
cargo test --workspace

# Single crate
cargo test -p s1-format-docx

# With output
cargo test -p s1-model -- --nocapture

Test Structure

  • Unit tests: #[cfg(test)] mod tests in each source file
  • Integration tests: tests/ directory in each crate
  • Property-based tests: proptest in s1-model and s1-ops
  • Round-trip tests: write → read → compare in format crates
  • CRDT tests: convergence and error path tests in s1-crdt

Writing Tests

#![allow(unused)]
fn main() {
#[test]
fn my_feature_works() {
    let mut doc = DocumentModel::new();
    let body_id = doc.body_id().unwrap();
    // ... setup ...
    assert_eq!(result, expected);
}
}

Format Round-Trip Tests

#![allow(unused)]
fn main() {
#[test]
fn roundtrip_feature() {
    let mut doc = make_doc_with_feature();
    let bytes = write(&doc).unwrap();
    let doc2 = read(&bytes).unwrap();
    // Verify feature survived round-trip
    assert_eq!(doc2.feature(), doc.feature());
}
}

Before Submitting

cargo test --workspace     # All tests pass
cargo clippy --workspace -- -D warnings  # No warnings
cargo fmt --check          # Formatting correct