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.
| Capability | Description |
|---|---|
| Multi-format | DOCX, ODT, PDF, TXT, Markdown, XLSX, ODS, CSV, legacy DOC |
| Pure Rust | Zero C/C++ dependencies. Compiles to native, WASM, and C FFI |
| Collaborative | Fugue CRDT for conflict-free multi-user editing |
| Layout engine | Pagination, text shaping (rustybuzz), font subsetting, PDF export |
| Web editor | Production-grade browser editor with toolbar, comments, track changes |
| PDF viewer | Built-in PDF viewer with annotations (highlight, comment, draw, redact) |
| Spreadsheet | Canvas-based spreadsheet with 60+ formulas, charts, and real-time collab |
| AI integration | Optional AI sidecar (llama.cpp with Qwen2.5-3B) for document assistance |
| Self-hostable | Single Docker image with white-labeling support |
| Embeddable | Rust 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 Rust | cargo add s1engine |
| Process documents in JS (no UI) | npm install @rudra/sdk |
| Embed an editor in React | npm install @rudra/react |
| Embed an editor in Vue | npm install @rudra/vue |
| Run a document API server | docker run rudra/server |
| Self-host the full platform | docker 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
Links
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
| Platform | Requirements |
|---|---|
| Rust | Rust 1.88+ via rustup |
| WASM | Node.js 18+, wasm-pack |
| Docker | Docker 20+ |
Choose Your Path
- Rust Library — Server-side document processing
- npm / WASM — Browser-based editing
- Docker — Full editor, zero setup
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
| Feature | Description | Default |
|---|---|---|
docx | DOCX read/write | Yes |
odt | ODT read/write | Yes |
txt | Plain text | Yes |
md | Markdown (GFM) | Yes |
pdf | PDF export | No |
crdt | Collaboration | No |
convert | Format conversion | No |
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
| From | DOCX | ODT | TXT | MD | |
|---|---|---|---|---|---|
| DOCX | - | Yes | Yes | Yes | Yes |
| ODT | Yes | - | Yes | Yes | Yes |
| TXT | Yes | Yes | Yes | - | Yes |
| MD | Yes | Yes | Yes | Yes | - |
| DOC | Yes | Yes | Yes | Yes | Yes |
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
- Each client maintains a local CRDT document
- Local edits generate operations with unique IDs
- Operations are broadcast to all peers via WebSocket
- Each peer applies remote operations to their local CRDT
- 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>
Logo
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., usescurrentColor), 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 (updatename,short_name,icons,theme_color)- Update
<meta name="theme-color" content="#...">inindex.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)
| Property | Default | Purpose |
|---|---|---|
--bg-app | #f8f9fa | Application background (behind the document canvas) |
--bg-white | #fff | Surface background (panels, cards) |
--bg-toolbar | #edf2fa | Toolbar and menu bar background |
--bg-toolbar-hover | #d3e3fd | Toolbar button hover state |
--bg-active | #c2dbff | Active/pressed toolbar button background |
--bg-hover | #f1f3f4 | General hover background for list items |
--accent-bg | #c2dbff | Accent background (selection highlights, badges) |
--border | #c4c7c5 | Primary border color |
--border-light | #dadce0 | Subtle border color (dividers, separators) |
--text-primary | #202124 | Primary text color |
--text-secondary | #5f6368 | Secondary text color (labels, descriptions) |
--text-muted | #80868b | Muted text (placeholders, disabled items) |
--accent | #1a73e8 | Primary accent color (buttons, links, active states) |
--accent-light | #d2e3fc | Light accent (selected item backgrounds) |
--danger | #d93025 | Error / destructive action color |
--success | #188038 | Success / confirmation color |
Layout Properties
| Property | Default | Purpose |
|---|---|---|
--radius-sm | 4px | Small border radius (buttons, inputs) |
--radius-md | 8px | Medium border radius (cards, panels, modals) |
--font-ui | 'Google Sans', 'Segoe UI', Roboto, ... | UI font stack |
Elevation (Shadows)
| Property | Default | Purpose |
|---|---|---|
--shadow-sm | 0 1px 2px rgba(60,64,67,.3), ... | Toolbar, small dropdowns |
--shadow-md | 0 1px 3px rgba(60,64,67,.3), ... | Panels, menus |
--shadow-lg | 0 1px 3px rgba(60,64,67,.3), ... | Modals, dialogs |
Z-Index Hierarchy
| Property | Default | Purpose |
|---|---|---|
--z-toolbar | 100 | Toolbar and menu bar |
--z-dropdown | 200 | Dropdown menus |
--z-sidebar | 300 | Side panels (pages, properties, comments) |
--z-modal-backdrop | 400 | Modal backdrop overlay |
--z-modal | 500 | Modal dialogs |
--z-context-menu | 600 | Right-click context menus |
--z-toast | 700 | Toast 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:
- Automatic – Follows the operating system preference via
prefers-color-scheme: dark. - Manual toggle – Users click the dark mode button in the status bar, which sets
data-theme="dark"on the<html>element. - 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
localStorageunder the keys1-theme.
Dark Theme Properties
| Property | Dark Value | Light 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
| ID | Function | Collapsible Class |
|---|---|---|
fontFamily | Font family dropdown | tb-collapsible-768 |
fontSize | Font size input | tb-collapsible-768 |
btnBold | Bold | – |
btnItalic | Italic | – |
btnUnderline | Underline | tb-collapsible-480 |
btnStrike | Strikethrough | tb-collapsible-1024 |
btnFormatPainter | Format Painter | tb-collapsible-1024 |
btnSuperscript | Superscript | tb-collapsible-1024 |
btnSubscript | Subscript | tb-collapsible-1024 |
colorPicker | Text color | tb-collapsible-1024 |
highlightPicker | Highlight color | tb-collapsible-1024 |
btnClearFormat | Clear formatting | tb-collapsible-1024 |
btnAlignL | Align left | tb-collapsible-480 |
btnAlignC | Align center | tb-collapsible-480 |
btnAlignR | Align right | tb-collapsible-480 |
btnAlignJ | Justify | tb-collapsible-480 |
lineSpacing | Line spacing dropdown | tb-collapsible-1024 |
btnOutdent | Decrease indent | tb-collapsible-1024 |
btnIndent | Increase indent | tb-collapsible-1024 |
btnBulletList | Bulleted list | tb-collapsible-480 |
btnNumberList | Numbered list | tb-collapsible-480 |
btnInsertMenu | Insert 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 1024pxtb-collapsible-768– Hidden below 768pxtb-collapsible-480– Hidden below 480px
You can adjust these breakpoints or assign different classes to control which items collapse first.
Menu Bar Items
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
| Flag | Default | Effect |
|---|---|---|
enableCollab | true | Enables the Share button, collaboration status indicator, and real-time editing. |
enablePdfViewer | true | Enables the built-in PDF viewer tab for opening PDF files. |
enableTemplates | true | Enables “New from Template” and “Save as Template” in the File menu. |
enableAI | true | Enables 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
- Load the font via a
<link>tag or@font-facerule:
<link href="https://fonts.googleapis.com/css2?family=Lora:wght@400;700&display=swap" rel="stylesheet">
- Add the option to the
fontFamilyselect inindex.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
titleattributes 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:
- Replace HTML strings – Edit
index.htmlto translate menu labels, button text, and tooltips. - 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
});
- 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
| File | Purpose |
|---|---|
/assets/logo.svg | Title bar logo |
/icon-192.svg | Favicon and PWA icon |
/manifest.json | PWA 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
| Property | Type | Default | Description |
|---|---|---|---|
apiUrl | string | '' | Base URL for the REST API. When empty, the editor operates in standalone mode with no server calls. |
relayUrl | string | '' | WebSocket URL for collaboration. When empty, real-time editing is disabled. |
mode | string | 'standalone' | 'standalone' or 'integrated'. Integrated mode enables server-backed file management. |
autoCollab | boolean | true | Connect to the collaboration room automatically when the page loads. |
productName | string | 'Rudra Office' | Display name used in the title bar, dialogs, and branding. |
enableCollab | boolean | true | Show or hide collaboration controls (Share button, peer indicators). |
enablePdfViewer | boolean | true | Allow PDF files to be opened in the viewer. |
enableTemplates | boolean | true | Show the template gallery when creating a new document. |
enableAI | boolean | true | Enable the AI writing assistant sidebar. |
aiUrl | string | '' | 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:
s1.tomlfile in the working directory (TOML format)- Environment variables prefixed with
S1_(fallback when no TOML file is found)
Environment variables are the recommended approach for container deployments.
Core Settings
| Variable | TOML Key | Default | Description |
|---|---|---|---|
S1_PORT | port | 8080 | HTTP port the server listens on. |
S1_STORAGE | storage | local | Storage backend: local, memory, or s3. |
S1_DATA_DIR | data_dir | ./data | Directory for local storage. Documents are stored as {id}.bin with {id}.meta.json sidecars. |
S1_STATIC_DIR | – | ./public | Directory containing the built editor frontend (HTML, JS, CSS, WASM). |
RUST_LOG | – | s1_server=info,tower_http=info | Log 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
| Variable | Default | Description |
|---|---|---|
S1_AUTH_ENABLED | false | Enable authentication middleware. When false, all endpoints are public. |
S1_JWT_SECRET | – | HMAC-SHA256 secret for validating JWT tokens. Required for integration mode (/edit?token=). |
S1_REQUIRE_JWT_EXP | false | Reject JWT tokens that lack an exp (expiry) claim. Recommended for production. |
S1_ALLOW_ANONYMOUS | true | Allow unauthenticated requests (Viewer-level access). |
When authentication is enabled, the server supports two credential methods:
- JWT Bearer tokens –
Authorization: Bearer <token>. The token payload must includesub(user ID) andname. Optional claims:role(adminoruser),tenant_id,exp. - API keys –
X-API-Key: <key>. Configured via theapi_keysarray in the auth config.
Admin Panel
| Variable | Default | Description |
|---|---|---|
S1_ADMIN_USER | – | Admin panel username. Both S1_ADMIN_USER and S1_ADMIN_PASS must be set to enable the admin panel. |
S1_ADMIN_PASS | – | Admin 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>.
| Variable | Default | Description |
|---|---|---|
S1_JWT_SECRET | – | Shared secret between the host application and rudra-server for signing/validating integration JWTs. |
S1_CALLBACK_SECRET | – | HMAC-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
| Claim | Required | Description |
|---|---|---|
file_id | Yes | Unique identifier for the document. |
user_id | Yes | Identifier of the editing user. |
user_name | Yes | Display name shown in the collaboration UI. |
permissions | No | Access level: edit (default), comment, or view. |
download_url | No | URL from which the server fetches the document on first open. Subject to SSRF protection. |
callback_url | No | URL the server POSTs the final document to when the editing session closes. |
exp | No | Token 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
| Parameter | Default | Description |
|---|---|---|
user | Random User-XXXXXXXX | Display name for presence. |
uid | Random UUID (8 chars) | Unique peer identifier for this connection. |
mode | edit | Editing mode for this peer. |
access | edit | Access 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
| Flag | Effect When Disabled |
|---|---|
enableCollab | Hides the Share button, peer count indicator, and collaboration status bar. WebSocket connections are not initiated. |
enablePdfViewer | PDF files cannot be opened. The editor shows an “unsupported format” message instead. |
enableTemplates | The template gallery is hidden. New documents start with a blank page. |
enableAI | The AI assistant panel and toolbar button are hidden. No requests are made to the AI sidecar. |
Server-Side Flags
| Variable | Effect |
|---|---|
S1_AUTH_ENABLED=true | Enforces JWT/API key authentication on all API endpoints. |
S1_REQUIRE_JWT_EXP=true | Rejects tokens without an exp claim, preventing indefinitely-valid tokens. |
S1_ALLOW_ANONYMOUS=false | Returns 401 Unauthorized for requests without credentials. |
URL Parameters
The editor reads several URL parameters to control its behavior on load.
| Parameter | Example | Description |
|---|---|---|
file | ?file=abc-123 | Open an existing file session by ID. The editor fetches the document from the server and connects to the collaboration room. |
type | ?type=sheet | Hint the editor to open in spreadsheet mode (for multi-app routing). Values: doc, sheet, slide. |
room | ?room=my-room | Join a specific collaboration room. Used with autoCollab: true. |
mode | ?mode=view | Set the editor to a specific access mode: edit, comment, or view. |
token | /edit?token=JWT | Integration 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
- Host application creates a file session via
/edit?token=JWTwith acallback_urlin the JWT claims. - Users edit the document collaboratively.
- When the session ends, the server POSTs the final document bytes to the callback URL.
Callback Request Headers
| Header | Description |
|---|---|
Content-Type | application/octet-stream |
X-S1-File-Id | The file session ID. |
X-S1-Event | Event type: session.closed (automatic) or document.saved (manual trigger via POST /api/v1/files/{id}/save). |
X-S1-Timestamp | Unix timestamp of the callback. |
X-S1-Signature | sha256=<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
| Event | Trigger |
|---|---|
document.created | A new document is uploaded or created. |
document.deleted | A document is deleted from storage. |
document.exported | A 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.
| Endpoint | URL |
|---|---|
| Editor UI | http://localhost:8080/ |
| REST API | http://localhost:8080/api/v1/ |
| WebSocket collab | ws://localhost:8080/ws/edit/{file_id} |
| Admin panel | http://localhost:8080/admin/dashboard |
| Health check | http://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:
| Tag | Description | Example |
|---|---|---|
latest | Latest stable release | rudra/server:latest |
X.Y.Z | Specific version (immutable) | rudra/server:1.0.2 |
X.Y | Latest patch within a minor release | rudra/server:1.0 |
X | Latest minor within a major release | rudra/server:1 |
edge | Latest 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
| Variable | Default | Description |
|---|---|---|
S1_PORT | 8080 | TCP port the server listens on. |
S1_STORAGE | local | Storage backend. One of local, memory, or s3. |
S1_DATA_DIR | /data | Directory for local file storage. Only used when S1_STORAGE=local. |
S1_STATIC_DIR | /app/public | Directory containing the editor static files (HTML, JS, CSS, WASM). |
RUST_LOG | s1_server=info | Log level filter. Uses tracing-subscriber EnvFilter syntax. |
Storage Backends
| Variable | Default | Description |
|---|---|---|
S1_STORAGE_POSTGRES_URL | – | PostgreSQL connection string for document metadata. Example: postgresql://user:pass@host:5432/dbname. |
S1_STORAGE_S3_ENDPOINT | – | S3-compatible endpoint URL. Example: http://minio:9000 or https://s3.amazonaws.com. |
S1_STORAGE_S3_BUCKET | – | S3 bucket name for document storage. |
S1_STORAGE_S3_ACCESS_KEY | – | S3 access key ID. |
S1_STORAGE_S3_SECRET_KEY | – | S3 secret access key. |
S1_STORAGE_S3_REGION | us-east-1 | S3 region (required by some providers). |
Authentication
| Variable | Default | Description |
|---|---|---|
S1_AUTH_ENABLED | false | Enable JWT and API key authentication. When false, all endpoints are public. |
S1_JWT_SECRET | – | HMAC-SHA256 secret for validating JWT tokens. Must be set when S1_AUTH_ENABLED=true. Also used for integration mode (/edit?token=). |
S1_REQUIRE_JWT_EXP | false | Reject JWT tokens that lack an exp (expiry) claim. Recommended for production. |
S1_ALLOW_ANONYMOUS | true | Allow unauthenticated requests with read-only (Viewer) access. Set to false in production. |
S1_CALLBACK_SECRET | – | HMAC-SHA256 key for signing callback request bodies. When set, outgoing callbacks include an X-S1-Signature: sha256=... header for verification. |
Collaboration
| Variable | Default | Description |
|---|---|---|
S1_COLLAB_ENABLED | true | Enable real-time collaborative editing via WebSocket. |
S1_COLLAB_REDIS_URL | – | Redis URL for cross-instance collab room routing. Required when running multiple server instances. |
Admin Panel
| Variable | Default | Description |
|---|---|---|
S1_ADMIN_USER | admin | Username for the admin panel at /admin/dashboard. |
S1_ADMIN_PASS | admin | Password for the admin panel. Change this in production. |
AI Sidecar
| Variable | Default | Description |
|---|---|---|
AI_PORT | 8081 | Port for the AI sidecar HTTP API. |
AI_CPUS | 4 | CPU core limit for the AI container. |
AI_MEMORY | 4G | Memory limit for the AI container. |
Volumes and Data Persistence
The following volumes should be persisted to avoid data loss:
| Volume | Container Path | Purpose |
|---|---|---|
rudra-data | /data | Document files and metadata (when S1_STORAGE=local). |
pgdata | /var/lib/postgresql/data | PostgreSQL database. |
redis-data | /data | Redis AOF persistence (collaboration state). |
minio-data | /data | MinIO 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
Unified Image (Recommended)
The Dockerfile.unified performs a multi-stage build:
- Stage 1 (rust-builder): Compiles the
s1-serverRust binary and the WASM bindings usingwasm-pack. - Stage 2 (web-builder): Installs npm dependencies and builds the editor frontend with Vite.
- Stage 3 (runtime): Copies the stripped server binary (~15 MB) and the
built static assets into a minimal
debian:bookworm-slimimage.
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
| Argument | Default | Description |
|---|---|---|
RUST_VERSION | 1.88 | Rust toolchain version used in the builder stage. |
NODE_VERSION | 20 | Node.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
/healthon 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
| Metric | Scale Up When | Scale 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
Recommended Resources
| Component | CPU | Memory | Disk |
|---|---|---|---|
| rudra/server | 1-2 cores | 256-512 MB | 1 GB (binary + static files) |
| PostgreSQL | 1 core | 256-512 MB | Depends on document count |
| Redis | 0.5 core | 128 MB | Minimal (in-memory state) |
| MinIO | 1 core | 512 MB | Depends on total document size |
| AI sidecar | 4 cores | 4 GB | 3 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
/datavolume 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: UpgradeandUpgrade: websocketheaders reach the server.
Admin Panel Returns 403
S1_ADMIN_USERandS1_ADMIN_PASSmust both be set and non-empty.- Sessions expire after 1 hour. Log in again.
JWT Authentication Rejected
- Ensure
S1_JWT_SECRETmatches the secret used to sign tokens. - If
S1_REQUIRE_JWT_EXP=true, all tokens must include anexpclaim. - Check that the token has not expired (
expmust 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.memoryin 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
| Component | Minimum | Recommended |
|---|---|---|
| CPU | 1 vCPU | 2+ vCPUs |
| RAM | 512 MB | 2 GB |
| Disk | 1 GB | 10 GB+ (depends on document volume) |
| Network | 1 Mbps | 100 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
| Dependency | Version | Notes |
|---|---|---|
| Docker | 20.10+ | For container deployments. Docker Compose v2 recommended. |
| Linux | Any modern distro | The container image uses debian:bookworm-slim. |
| curl | Any | Used 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
Docker Compose (recommended)
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=trueto reject tokens without expiry. - Set
S1_AUTH_ENABLED=trueif the instance is not behind an authenticating proxy. - Set
S1_ALLOW_ANONYMOUS=falseto prevent unauthenticated access. - Change
S1_ADMIN_USERandS1_ADMIN_PASSfrom their defaults. - Set
S1_CALLBACK_SECRETif 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(oralways) in your Compose file. - Configure log rotation (Docker logging driver or external collector).
Performance
- Set
RUST_LOG=s1_server=info,tower_http=warnto reduce log volume. - Set appropriate resource limits in Docker (
cpus,memory). - Monitor the
/admin/api/healthendpoint 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
| Endpoint | Auth Required | Description |
|---|---|---|
GET /health | No | Basic health check. Returns {"status":"ok","version":"..."}. Use this for load balancer probes and Docker health checks. |
GET /admin/api/health | Yes (admin) | Detailed health metrics including uptime, memory usage, active sessions, active rooms, total editors, and PID. |
GET /admin/api/stats | Yes (admin) | Server statistics: uptime, session count, editor count, memory. |
GET /api/v1/info | No | Server 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
| Metric | Source | Alert Threshold |
|---|---|---|
| HTTP response status | Reverse proxy logs | Error 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 usage | Host 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
- Back up data – Create a snapshot of the data volume before upgrading.
- Check release notes – Look for breaking changes in configuration or storage format.
- 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
.localor.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
| Type | Description | Free Function |
|---|---|---|
S1Engine | Engine instance | s1_engine_free |
S1Document | Open document | s1_document_free |
S1Error | Error message | s1_error_free |
S1String | UTF-8 string | s1_string_free |
S1Bytes | Byte buffer | s1_bytes_free |
REST API
The rudra-server provides a REST API for document management.
Base URL
http://localhost:8080/api/v1/
Endpoints
Documents
| Method | Endpoint | Description |
|---|---|---|
POST | /documents | Upload a document (multipart) |
GET | /documents | List documents (paginated) |
GET | /documents/:id | Get document metadata |
GET | /documents/:id/content | Download document bytes |
DELETE | /documents/:id | Delete a document |
GET | /documents/:id/thumbnail | Get PDF thumbnail |
Conversion
| Method | Endpoint | Description |
|---|---|---|
POST | /convert | Stateless format conversion |
Webhooks
| Method | Endpoint | Description |
|---|---|---|
POST | /webhooks | Register a webhook |
GET | /webhooks | List webhooks |
DELETE | /webhooks/:id | Delete a webhook |
System
| Method | Endpoint | Description |
|---|---|---|
GET | /health | Health check |
GET | /info | Server 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
- Client connects to
ws://host/ws/collab/my-doc-123 - Server sends welcome:
{"type":"welcome","roomId":"my-doc-123","opsCount":0} - If late joiner, server sends catch-up ops:
{"type":"catchUp","op":{...}} - Client sends operations as JSON text messages
- 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
- s1-model has zero external dependencies — pure Rust data structures
- All mutations via Operations — never modify the tree directly
- Format isolation — each format crate only depends on s1-model
- No panics in library code — all public functions return
Result - 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
| Type | Description | Can Contain |
|---|---|---|
| Document | Root node | Body, Header, Footer, CommentBody |
| Body | Main content | Paragraph, Table, TOC, PageBreak |
| Paragraph | Text container | Run, Image, LineBreak, Tab, Field |
| Run | Formatting span | Text |
| Text | Leaf text content | (none) |
| Table | Grid container | TableRow |
| TableRow | Row | TableCell |
| TableCell | Cell | Paragraph |
| Image | Inline image | (none) |
NodeId
Every node has a globally unique NodeId(replica_id, counter):
replica_ididentifies the creating peer (for CRDT)counteris 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
titletooltip - 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 testsin each source file - Integration tests:
tests/directory in each crate - Property-based tests:
proptestin 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