Headless Univer
Headless Univer is Univer’s content runtime without an interactive editor UI. It shares content models, commands, calculation, and Facade APIs with the browser editor. It can process content in Node.js or run UI-free tasks in a browser. Available capabilities depend on the registered product and feature plugins.
In Node.js, you can create or load documents, read and edit paragraphs and text, change structure and styles, and save Univer snapshots. Tables, lists, columns, formulas, and shapes require their content plugins. Content edits do not produce page screenshots.
The example below uses plugin mode. @univerjs/core and product content plugins belong to the Headless content layer; @univerjs/ui, product *-ui packages, and default browser core presets do not belong in this configuration. The example retains @univerjs/engine-render as a shared dependency without mounting a Canvas or editor. See Headless package boundaries for the classification and exceptions.
Local snapshot operations need no collaboration service. The collaboration example additionally needs a client, server, and authentication; Office conversion and screenshots require Server SDK and a rendering runtime, respectively. Importing /facade alone neither registers feature plugins nor makes UI APIs Headless APIs.
Installation
pnpm add @univerjs/core @univerjs/engine-render @univerjs/engine-formula @univerjs/docsnpm install @univerjs/core @univerjs/engine-render @univerjs/engine-formula @univerjs/docsyarn add @univerjs/core @univerjs/engine-render @univerjs/engine-formula @univerjs/docsbun add @univerjs/core @univerjs/engine-render @univerjs/engine-formula @univerjs/docsUsage
import { LocaleType, Univer } from '@univerjs/core'import { FUniver } from '@univerjs/core/facade'import { UniverDocsPlugin } from '@univerjs/docs'import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'import { UniverRenderEnginePlugin } from '@univerjs/engine-render'import '@univerjs/docs/facade'const univer = new Univer({ locale: LocaleType.EN_US, locales: { [LocaleType.EN_US]: {}, },})univer.registerPlugin(UniverRenderEnginePlugin)univer.registerPlugin(UniverFormulaEnginePlugin)univer.registerPlugin(UniverDocsPlugin)const univerAPI = FUniver.newAPI(univer)univerAPI.createDocument({})Collaborative editing in Node.js
Node.js can act as a collaboration client: load an existing document from the server, modify it through the Facade API, and synchronize changes to other clients with the same document open. Before running the example, prepare a collaboration service, a client license, and an accessible document ID.
Example files
Organize the files as follows:
src/docs/node/├── main.ts└── univer.tsuniver.ts: Create Univer and register document feature plugins, Facade extensions, and the Node.js collaboration client.main.ts: Load a collaborative document and demonstrate paragraphs, text, headers and footers, callouts, code blocks, quotes, lists, shapes, tables, and columns.
Replace your-license, the server URL, and the Cookie in univer.ts, as well as the document ID in main.ts.
import { UniverCollaborationPlugin } from '@univerjs-pro/collaboration'import { UniverCollaborationClientPlugin } from '@univerjs-pro/collaboration-client'import { NodeCollaborationSocketService, UniverCollaborationClientNodePlugin,} from '@univerjs-pro/collaboration-client-node'import { UniverDocsCalloutPlugin } from '@univerjs-pro/docs-callout'import { UniverDocsCodePlugin } from '@univerjs-pro/docs-code'import { UniverDocsColumnPlugin } from '@univerjs-pro/docs-column'import { UniverDocsLatexPlugin } from '@univerjs-pro/docs-latex'import { UniverDocsListPlugin } from '@univerjs-pro/docs-list'import { UniverDocsQuotePlugin } from '@univerjs-pro/docs-quote'import { UniverDocsReferencePlugin } from '@univerjs-pro/docs-reference'import { UniverDocsShapePlugin } from '@univerjs-pro/docs-shape'import { UniverDocsTablePlugin } from '@univerjs-pro/docs-table'import { UniverLicensePlugin } from '@univerjs-pro/license'import { IAuthzIoService, IImageIoService, IMentionIOService, IUndoRedoService, LocaleType, Univer,} from '@univerjs/core'import { UniverDocsPlugin } from '@univerjs/docs'import { UniverDocsDrawingPlugin } from '@univerjs/docs-drawing'import { UniverDocsHyperLinkPlugin } from '@univerjs/docs-hyper-link'import { UniverDrawingPlugin } from '@univerjs/drawing'import { UniverRenderEnginePlugin } from '@univerjs/engine-render'import { UniverNetworkPlugin } from '@univerjs/network'import { UniverThreadCommentPlugin } from '@univerjs/thread-comment'import '@univerjs/docs/facade'import '@univerjs/docs-drawing/facade'import '@univerjs-pro/docs-shape/facade'import '@univerjs-pro/docs-callout/facade'import '@univerjs-pro/docs-reference/facade'import '@univerjs-pro/docs-code/facade'import '@univerjs-pro/docs-list/facade'import '@univerjs-pro/docs-quote/facade'import '@univerjs-pro/docs-table/facade'import '@univerjs-pro/docs-column/facade'import '@univerjs-pro/docs-latex/facade'import '@univerjs-pro/collaboration-client/facade'export function createUniverOnNode(): Univer { const univer = new Univer({ locale: LocaleType.EN_US, locales: {}, override: [ [IAuthzIoService, null], [IUndoRedoService, null], [IMentionIOService, null], ], }) univer.registerPlugin(UniverLicensePlugin, { license: 'your-license', }) univer.registerPlugin(UniverRenderEnginePlugin) univer.registerPlugin(UniverThreadCommentPlugin) univer.registerPlugin(UniverDrawingPlugin, { override: [[IImageIoService, null]], }) univer.registerPlugin(UniverDocsPlugin) univer.registerPlugin(UniverDocsDrawingPlugin) univer.registerPlugin(UniverDocsHyperLinkPlugin) univer.registerPlugin(UniverDocsReferencePlugin) univer.registerPlugin(UniverDocsCalloutPlugin) univer.registerPlugin(UniverDocsCodePlugin) univer.registerPlugin(UniverDocsListPlugin) univer.registerPlugin(UniverDocsQuotePlugin) univer.registerPlugin(UniverDocsShapePlugin) univer.registerPlugin(UniverDocsTablePlugin) univer.registerPlugin(UniverDocsColumnPlugin) univer.registerPlugin(UniverDocsLatexPlugin) registerCollaborationPlugins(univer) return univer}function registerCollaborationPlugins(univer: Univer): void { const serverUrl = 'https://your-univer-server.example.com' univer.registerPlugin(UniverNetworkPlugin) univer.registerPlugin(UniverCollaborationPlugin) univer.registerPlugin(UniverCollaborationClientPlugin, { socketService: NodeCollaborationSocketService, enableOfflineEditing: false, enableSingleActiveInstanceLock: false, snapshotServerUrl: `${serverUrl}/universer-api/snapshot`, collabSubmitChangesetUrl: `${serverUrl}/universer-api/comb`, collabWebSocketUrl: `${serverUrl}/universer-api/comb/connect`, wsSessionTicketUrl: `${serverUrl}/universer-api/user/session-ticket`, uploadFileServerUrl: `${serverUrl}/universer-api/stream/file/upload`, signUrlServerUrl: `${serverUrl}/universer-api/file/{fileID}/sign-url`, downloadEndpointUrl: serverUrl, authzUrl: `${serverUrl}/universer-api/authz`, sendChangesetTimeout: 200, retryConnectingInterval: 1000, customHeaders: { // Example: If your Univer server requires authentication via cookies, you can set the 'cookie' header here. cookie: 'your-cookie', // ... more headers }, }) univer.registerPlugin(UniverCollaborationClientNodePlugin)}import type { FConnectorShape } from '@univerjs-pro/engine-shape/facade'import { FUniver } from '@univerjs/core/facade'import process from 'node:process'import { createUniverOnNode } from './univer'async function run(): Promise<void> { const univer = createUniverOnNode() const univerAPI = FUniver.newAPI(univer) const document = await univerAPI.getCollaboration().loadDocAsync('your-unit-id') if (!document) { throw new Error('Failed to load the collaborative document. Check the document ID and access permissions.') } testDocsFacadeAPI(univerAPI) testDocsHeaderFooterFacadeAPI(univerAPI) testDocsCalloutFacadeAPI(univerAPI) testDocsCodeFacadeAPI(univerAPI) testDocsQuoteFacadeAPI(univerAPI) testDocsListFacadeAPI(univerAPI) testDocsShapeFacadeAPI(univerAPI) testDocsTableFacadeAPI(univerAPI) testDocsColumnFacadeAPI(univerAPI) await univerAPI.getCollaboration().flush(document.getId()) console.warn('Document changes have been synchronized to the server.')}function testDocsShapeFacadeAPI(univerAPI: FUniver) { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return const paragraph = fDocument.getParagraphs()[0] if (!paragraph) return const anchor = { paragraphId: paragraph.getId(), segmentId: paragraph.getSegmentId(), } const shape = fDocument.insertShape({ shapeType: univerAPI.Enum.ShapeTypeEnum.RoundRect, placement: { wrappingStyle: univerAPI.Enum.TextWrappingStyle.WRAP_SQUARE, anchor, position: { horizontalOffset: 80, verticalOffset: 80 }, }, transform: { width: 320, height: 160 }, shapeData: { fill: { fillType: univerAPI.Enum.ShapeFillEnum.SolidFill, color: '#DBEAFE', }, stroke: { lineStrokeType: univerAPI.Enum.ShapeLineTypeEnum.SolidLine, color: '#2563EB', width: 2, }, }, }) if (!shape) return shape .setRotation(4) .setStrokeOpacity(0.9) .getText() .setText('Document shape Facade example') .setHorizontalAlign(univerAPI.Enum.HorizontalAlign.CENTER) .setVerticalAlign(univerAPI.Enum.VerticalAlign.MIDDLE) const target = fDocument.insertShape({ shapeType: univerAPI.Enum.ShapeTypeEnum.Ellipse, placement: { wrappingStyle: univerAPI.Enum.TextWrappingStyle.WRAP_SQUARE, anchor, position: { horizontalOffset: 500, verticalOffset: 100 }, }, transform: { width: 160, height: 120 }, shapeData: { fill: { fillType: univerAPI.Enum.ShapeFillEnum.SolidFill, color: '#DCFCE7' }, stroke: { lineStrokeType: univerAPI.Enum.ShapeLineTypeEnum.SolidLine, color: '#16A34A', width: 2 }, }, }) if (!target) return const connector = fDocument.insertShape({ shapeType: univerAPI.Enum.ShapeTypeEnum.BentConnector3, placement: { wrappingStyle: univerAPI.Enum.TextWrappingStyle.WRAP_SQUARE, anchor, position: { horizontalOffset: 360, verticalOffset: 130 }, }, transform: { width: 180, height: 80 }, }) if (!connector || !connector.isConnectorShape()) return const sourceSites = shape.getConnectionSites() const targetSites = target.getConnectionSites() ;(connector as FConnectorShape) .setStrokeColor('#475569') .setStrokeWidth(2) .setEndArrow(univerAPI.Enum.ShapeArrowTypeEnum.Arrow, univerAPI.Enum.ShapeArrowSizeEnum.Medium) .bindStart(shape.getId(), sourceSites[0]?.index ?? 0) .bindEnd(target.getId(), targetSites[2]?.index ?? targetSites[0]?.index ?? 0) console.warn('Document shape ID:', shape.getId()) console.warn('Document shape count:', fDocument.getShapes().length)}function testDocsFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return const intro = fDocument.appendParagraph( `Greetings from the Node.js collaboration client. Current time: ${new Date().toString()}.`, ) intro.appendText(' [inline insert]') const beforeIntro = fDocument.insertParagraph( intro.getInfo().paragraphIndex, 'Inserted before the introduction using a paragraph handle.', ) const foundIntro = fDocument.findParagraphByText('Node.js collaboration client') const introMatches = fDocument.findParagraphs({ text: 'Node.js collaboration client' }) const introRange = intro.getRange() const bodyBeforeDelete = fDocument.getBody() const deleteStartOffset = bodyBeforeDelete.dataStream.indexOf(' [inline insert]') if (deleteStartOffset >= 0) { fDocument.deleteRange({ startOffset: deleteStartOffset, endOffset: deleteStartOffset + ' [inline insert]'.length, }) } intro.setStyle({ textStyle: { fs: 12, }, }) console.warn(`Current document ID: ${fDocument.getId()}`) console.warn(`Current document name: ${fDocument.getName()}`) console.warn(`Is the current document modern: ${fDocument.isModern()}`) console.warn(`Introduction paragraph ID: ${intro.getId()}`) console.warn(`Introduction paragraph child index after moving: ${intro.getInfo().paragraphIndex}`) console.warn('Introduction paragraph range:', introRange) console.warn('Found introduction paragraph ID:', foundIntro?.getId()) console.warn('Matching introduction paragraph count:', introMatches.length) console.warn('Body paragraph count:', fDocument.getParagraphs().length) console.warn(`Inserted paragraph ID: ${beforeIntro.getId()}`) console.warn(`Document snapshot body length: ${fDocument.save().body?.dataStream.length ?? 0}`)}function testDocsHeaderFooterFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return if (fDocument.isModern()) { console.warn('Skipping the header/footer example: modern documents do not support page headers and footers.') return } const headerSegmentId = fDocument.ensurePageHeader() const footerSegmentId = fDocument.ensurePageFooter() const headerParagraph = fDocument.appendParagraph('Docs Node.js header Facade example.', headerSegmentId) headerParagraph.appendText(` Document: ${fDocument.getName() || fDocument.getId()}`) const footerParagraph = fDocument.appendParagraph('Docs Node.js footer Facade example.', footerSegmentId) fDocument.insertText(0, 'Page header:', headerSegmentId) footerParagraph.setStyle({ textStyle: { fs: 10, }, }) console.warn('Document header segment ID:', headerSegmentId) console.warn( 'Document header paragraphs:', fDocument.getParagraphs(headerSegmentId).map((paragraph) => paragraph.getText()), ) console.warn('Document header body length:', fDocument.getBody(headerSegmentId).dataStream.length) console.warn('Document footer segment ID:', footerSegmentId) console.warn( 'Document footer paragraph ID:', fDocument.findParagraphByText('footer Facade example', footerSegmentId)?.getId(), )}function testDocsCalloutFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return const paragraph = fDocument.appendParagraph('Docs Node.js callout Facade example.') const callout = fDocument.insertCallout(paragraph, { blockId: createSampleId('callout'), config: { borderRadius: 8, icon: '!', }, }) if (!callout) return callout.updateConfig({ borderRadius: 8 }) console.warn('Inserted callout ID:', callout.getId()) console.warn('Document callout information:', callout.describe())}function testDocsCodeFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return const paragraph = fDocument.appendParagraph('const message = "Greetings from the Docs Node.js code block Facade";') const code = fDocument.insertCode(paragraph, { blockId: createSampleId('code'), config: { language: 'typescript', showLineNumbers: true, tabSize: 4, wrap: true, }, }) if (!code) return code.updateConfig({ config: { language: 'typescript', showLineNumbers: true, }, }) console.warn('Inserted code block ID:', code.getId()) console.warn('Document code block information:', code.describe())}function testDocsQuoteFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return const paragraph = fDocument.appendParagraph('Docs Node.js quote Facade example.') const quote = fDocument.insertQuote(paragraph, { blockId: createSampleId('quote'), }) if (!quote) return console.warn('Inserted quote ID:', quote.getId()) console.warn('Document quote information:', quote.describe())}function testDocsListFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return const paragraph = fDocument.appendParagraph('Docs Node.js list Facade example.') const success = fDocument.insertList(paragraph, { listType: univerAPI.Enum.PresetListType.BULLET_LIST, }) if (!success) return const item = fDocument.findListItemByText('list Facade example') if (!item) return item.setGlyphSymbol('-', { mode: univerAPI.Enum.DocsListSelectionMode.Item }) item.setGlyphType(univerAPI.Enum.ListGlyphType.BULLET, { mode: univerAPI.Enum.DocsListSelectionMode.Item }) console.warn('Document list item:', item.describe()) console.warn('Document list items:', fDocument.describeListItems())}function testDocsTableFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return const table = fDocument.insertTableFromData( [ ['Feature', 'Status', 'Owner'], ['Facade API', 'Ready', 'Docs Node.js client'], ['Collaboration', 'Loaded', 'Node.js client'], ], { headerRowCount: 1, tableId: createSampleId('table'), }, ) if (!table) return table.setCellText(1, 1, 'Updated') table.getCell(1, 2)?.setBackgroundColor('#E8F1FF') table.setColumnType(1, { type: univerAPI.Enum.DocsTableColumnType.Text }) table.setTableBorder({ color: '#3367D6', preset: univerAPI.Enum.DocsTableBorderPreset.All, width: 1, }) console.warn('Document table information:', table.describe()) console.warn('Table ID found by text:', fDocument.findTableByText('Facade API')?.getId())}function testDocsColumnFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return if (!fDocument.isModern()) { console.warn('Skipping the column example: column groups require a modern document.') return } const groupId = createSampleId('column-group') const group = fDocument.insertColumnGroup(3, { columnGroupId: groupId, columnIds: [createSampleId('column-left'), createSampleId('column-middle'), createSampleId('column-right')], gap: 18, widthRatios: [1.1, 1, 0.9], }) if (!group) return const [leftColumn, middleColumn, rightColumn] = group.getColumns() const leftOffset = leftColumn?.getInsertOffset() if (leftOffset != null) { fDocument.insertText(leftOffset, 'Column Facade left lane.') } const middleOffset = middleColumn?.getInsertOffset() if (middleOffset != null) { fDocument.insertText(middleOffset, 'Column Facade middle lane.') } const rightOffset = rightColumn?.getInsertOffset() if (rightOffset != null) { fDocument.insertTableFromData( [ ['Metric', 'Current', 'Next'], ['Feature coverage', 'Columns', 'Header/Footer'], ], { headerRowCount: 1, offset: rightOffset, tableId: createSampleId('column-table'), }, ) } group.setWidthRatios([1.2, 1, 0.8]) const insertedColumn = middleColumn ? group.addColumn( middleColumn.getId(), univerAPI.Enum.DocsColumnPositionEnum.RIGHT, createSampleId('column-inserted'), ) : null const insertedOffset = insertedColumn?.getInsertOffset() if (insertedOffset != null) { fDocument.insertText(insertedOffset, 'Column inserted through the Facade API.') } const foundByText = fDocument.findColumnGroupByText('middle lane') const foundById = fDocument.findColumnGroups({ columnGroupId: group.getId() }) const firstColumn = group.getColumn(0) const groupRange = group.getRange() const rangeLookup = groupRange ? fDocument.getColumnGroupAt(groupRange.startOffset) : null console.warn('Document column group information:', group.describe()) console.warn('Document column group count found by ID:', foundById.length) console.warn('Document column group ID found by text:', foundByText?.getId()) console.warn('Document column group ID found by range:', rangeLookup?.getId()) console.warn('Document first column text:', firstColumn?.getText())}function createSampleId(name: string): string { return `docs-node-${name}-${Date.now()}`}run().catch((error: unknown) => { console.error(error) process.exitCode = 1})Runtime notes
loadDocAsync() loads an existing server document; you do not need to call createDocument(). The collaboration service identifies the user and checks permissions through authentication information such as the Cookie in customHeaders. The server returns the connection’s memberID.
This example disables browser offline caching and the single-active-instance lock across tabs, so there is no need to call setCurrentUser() for cache isolation between accounts. If you later use features such as comments that depend on the local current user, you can set the application user through UserManagerService; this does not replace server authentication.
The header/footer example applies only to paged documents, while the column example applies only to modern documents. The example checks the document mode with isModern() and skips unsupported operations.
The example inserts content into an existing document, so use a test document. The example keeps the collaboration connection open after running. Open the same document in a browser to see the changes. flush() waits for server acknowledgement of queued collaboration changes. For a one-shot script, call univer.dispose() after all asynchronous operations and synchronization have finished.
How is this guide?