tiptap Documentation

The headless rich text editor framework for web artisans.

37,996 stars TypeScript TypeScript 1 file ยท ~13,539 tokens #editor#javascript#js#prosemirror#react#renderless-components#rich-text#tiptap
RAW Doc

1. Project Overview & Quickstart (ueberdosis/tiptap)

File: README.md








Tiptap Editor

The Tiptap Editor is a headless, framework-agnostic rich text editor that's customizable and extendable through extensions. Its headless nature means it comes without a set user interface, offering full design freedom (for a jumpstart, see linked UI templates below). Tiptap is based on the highly reliable ProseMirror library.

Tiptap Editor is complemented by the collaboration open-source backend Hocuspocus. Both the Editor and Hocuspocus form the foundation of the Tiptap Suite.

How does the Tiptap Editor work?

  • Headless Framework: Tiptap does not rely on a user interface. So there is no need for class overrides or code hacks. If you do need an example UI feel free to browse our UI templates linked below.
  • Framework-agnostic: The Tiptap Editor is designed to work across different frontend frameworks. This means whether you're using Vue, React, or plain JavaScript, Tiptap integrates without compatibility issues.
  • Extension based: Extensions in Tiptap allow for a tailored editing experience, from simple text styling to advanced features like drag-and-drop block editing. You have the option to choose from over 100 extensions available in the documentation and community to enhance your editor's functionality.
  • Customize your UX: The editor was built to give you control to define your own extensions and nodes.

Editor Pro Extensions

The Pro Extensions are a set of advanced functionalities that enhance the capabilities of the Tiptap Editor. They are additional features that can be integrated into the base editor to provide more sophisticated editing options.

Key functionalities include collaborative editing, commenting, versioning, document conversion and AI related features.
Review the docs right here.

Pro Extensions need a valid subscription.

Make your editor collaborative

Interested in collaborative editing? Check out our open-source package Hocuspocus - a collaboration backend built around the CRDT power of Yjs. Hocuspocus serves as the backbone for the Tiptap Suite.

Documentation

For more detailed information, make sure to check out our documentation. If you encounter any problems or have suggestions for our system, please open an issue.

Examples, CodeSandbox and UI Templates

Have a look at the examples to see Tiptap in action or review and fork our codesandboxes.

About Tiptap

Tiptap is a collection of developer components based on open-source technology, forming the basis of our advanced, paid features. It includes the open-source editor component, collaboration features, Content AI, and Tiptap Cloud. We are developing open-source products that also shape our paid features. We're committed to improving both, ensuring quality and reliability in every update.

For more details, visit the Tiptap documentation or website.

Community

For help, discussion about best practices, or any other conversation that would benefit from being searchable:

Discuss Tiptap on GitHub

Sponsors ๐Ÿ’–

Complish Storyblok PostHog Reflect Ziff Media Basewell Poggio

iFixit, ApostropheCMS, Novadiscovery, Omics Data Automation, Flow Mobile, DocIQ and hundreds of awesome individuals.

Contributing

Feel like adding some magic of your own to Tiptap Editor Core? We welcome contributions! Please see our CONTRIBUTING guidelines for how to get started.

Contributors

Sam Willis,
Brian Hung,
Dirk Holtwick,
Sam Duvall,
Christoph Flathmann,
Erick Wilder,
Marius Tolzmann,
jjangga0214,
Maya Nedeljkovich,
Ryan Bliss,
Gregor and many more.

License

The MIT License (MIT). Please see License File for more information.


File: .changeset/README.md

Changesets

Hello and welcome! This folder has been automatically generated by @changesets/cli, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it in our repository

We have a quick list of common questions to get you started engaging with this project in
our documentation


File: packages/vue-3/README.md

@tiptap/vue-3




Introduction

Tiptap is a headless wrapper around ProseMirror โ€“ a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as New York Times, The Guardian or Atlassian.

Official Documentation

Documentation can be found on the Tiptap website.

License

Tiptap is open sourced software licensed under the MIT license.


File: packages/vue-2/README.md

@tiptap/vue-2




Introduction

Tiptap is a headless wrapper around ProseMirror โ€“ a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as New York Times, The Guardian or Atlassian.

Official Documentation

Documentation can be found on the Tiptap website.

License

Tiptap is open sourced software licensed under the MIT license.


File: packages/suggestion/README.md

@tiptap/suggestion




Introduction

Tiptap is a headless wrapper around ProseMirror โ€“ a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as New York Times, The Guardian or Atlassian.

Official Documentation

Documentation can be found on the Tiptap website.

License

Tiptap is open sourced software licensed under the MIT license.


File: packages/static-renderer/README.md

@tiptap/static-renderer




Introduction

Tiptap is a headless wrapper around ProseMirror โ€“ a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as New York Times, The Guardian or Atlassian.

Official Documentation

Documentation can be found on the Tiptap website.

Limitations & workarounds

The static renderer builds the ProseMirror schema and runs each extension's
renderHTML, but it does not instantiate an Editor. As a result:

  • addProseMirrorPlugins, onCreate, onUpdate, and transaction hooks do
    not run.
  • Extensions that assign attributes via those mechanisms โ€” such as
    UniqueID (data-id) and TableOfContents (id, data-toc-id) โ€” will
    not populate those attributes on their own.

For these cases, pre-process the JSON document before rendering:

ts
import { generateUniqueIds } from '@tiptap/extension-unique-id'
import { generateTocIds } from '@tiptap/extension-table-of-contents'
import { renderToHTMLString } from '@tiptap/static-renderer/pm/html-string'

let doc = sourceJson
doc = generateUniqueIds(doc, extensions) // if using UniqueID
doc = generateTocIds(doc, extensions) // if using TableOfContents

const html = renderToHTMLString({
  content: doc,
  extensions,
  staticEditorOptions: { textDirection: 'auto' }, // mirrors a subset of EditorOptions
})

Editor-level options that affect output are accepted via the
staticEditorOptions object (currently textDirection). Other editor options
that depend on a runtime view or transaction stream are out of scope.

License

Tiptap is open sourced software licensed under the MIT license.


File: packages/starter-kit/README.md

@tiptap/starter-kit




Introduction

Tiptap is a headless wrapper around ProseMirror โ€“ a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as New York Times, The Guardian or Atlassian.

Official Documentation

Documentation can be found on the Tiptap website.

License

Tiptap is open sourced software licensed under the MIT license.


File: packages/react/README.md

@tiptap/react




Introduction

Tiptap is a headless wrapper around ProseMirror โ€“ a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as New York Times, The Guardian or Atlassian.

Official Documentation

Documentation can be found on the Tiptap website.

License

Tiptap is open sourced software licensed under the MIT license.


File: packages/pm/README.md

@tiptap/pm




Introduction

Tiptap is a headless wrapper around ProseMirror โ€“ a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as New York Times, The Guardian or Atlassian.

What is this `pm` package?

The pm package re-exports ProseMirror packages under a single version range. It bundles all ProseMirror packages that are required to run Tiptap into one consistent dependency, so you don't need to install and align multiple ProseMirror packages yourself.

Official Documentation

Documentation can be found on the Tiptap website.

License

Tiptap is open sourced software licensed under the MIT license.


File: packages/markdown/README.md

@tiptap/markdown




Introduction

Tiptap is a headless wrapper around ProseMirror โ€“ a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as New York Times, The Guardian or Atlassian.

Official Documentation

Documentation can be found on the Tiptap website.

License

Tiptap is open sourced software licensed under the MIT license.

2. Official Technical Reference & Guides (ueberdosis/tiptap-docs)

File: README.md

Tiptap Documentation

Welcome to the official documentation for all Tiptap products! Here, you'll find comprehensive guides, detailed examples, and in-depth documentation covering everything you need to know about our offerings.

Available Documentation

Our documentation includes resources for all our major products:

  • Editor: Learn how to integrate, customize, and extend the Tiptap editor.
  • Collaboration: Find out how to enable real-time collaboration features in your projects.
  • Document AI: Discover how to leverage AI for document processing and automation.
  • Comments: Implement and manage a robust commenting system.

Explore the full documentation at tiptap.dev.

Contributing

We greatly appreciate and encourage contributions from the community. If you're interested in contributing to our documentation, please make sure to read and follow our contributing guidelines. Your efforts help us improve and keep our documentation up to date, ensuring that everyone has access to the best resources possible.

Thank you for being a part of the Tiptap community!

License

This project is licensed under the Tiptap Documentation Software License license. This applies to all files except those in the src/content directory, which contains the documentation content or the public/assets/fonts directory which contains our fonts. The documentation content is licensed under the MIT license. The fonts licenses can be found in src/assets/fonts/LICENSE.txt.


File: .changeset/README.md

Changesets

Hello and welcome! This folder has been automatically generated by @changesets/cli, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it in our repository

We have a quick list of common questions to get you started engaging with this project in
our documentation


File: src/content/hocuspocus/server/extensions/database.mdx


title: Database
meta:
title: Database | Hocuspocus Docs
category: Hocuspocus
tags:


Store your data in whatever data store you already have with the generic database extension.
It takes a Promise to fetch data and another Promise to store the data, thatโ€™s all. Hocuspocus will handle the rest.

Installation

Install the database extension like this:

bash
npm install @hocuspocus/extension-database

Configuration

fetch

Expects an async function (or Promise) which returns a Y.js compatible Uint8Array (or null).
Make sure to return the same Uint8Array that was saved in store(), and do not create a new Ydoc,
as doing so would lead to a new history (and duplicated content).

If you want to initially create a Ydoc based off raw text/json, you can do so here using a transformer of your choice
(e.g. TiptapTransformer.toYdoc, or ProsemirrorTransformer.toYdoc)

store

Expects an async function (or Promise) which persists the Y.js binary data somewhere.

Usage

The following example uses SQLite to store and retrieve data. You can replace that part with whatever data store you
have. As long as you return a Promise you can store data with PostgreSQL, MySQL, MongoDB, S3 โ€ฆ If you actually want to
use SQLite, you can have a look at the SQLite extension.

js
import { Server } from "@hocuspocus/server";
import { Database } from "@hocuspocus/extension-database";
import sqlite3 from "sqlite3";

const server = new Server({
  extensions: [
    new Database({
      // Return a Promise to retrieve data โ€ฆ
      fetch: async ({ documentName }) => {
        return new Promise((resolve, reject) => {
          this.db?.get(
            `
            SELECT data FROM "documents" WHERE name = $name ORDER BY rowid DESC
          `,
            {
              $name: documentName,
            },
            (error, row) => {
              if (error) {
                reject(error);
              }

              resolve(row?.data);
            }
          );
        });
      },
      // โ€ฆ and a Promise to store data:
      store: async ({ documentName, state }) => {
        this.db?.run(
          `
          INSERT INTO "documents" ("name", "data") VALUES ($name, $data)
            ON CONFLICT(name) DO UPDATE SET data = $data
        `,
          {
            $name: documentName,
            $data: state,
          }
        );
      },
    }),
  ],
});

server.listen();

File: src/content/hocuspocus/server/extensions/logger.mdx


title: Logger
meta:
title: Logger | Hocuspocus Docs
category: Hocuspocus
tags:


Hocuspocus doesnโ€™t log anything. Thanks to this simple extension it will.

Installation

Install the Logger package with:

bash
npm install @hocuspocus/extension-logger

Configuration

Instance name

You can prepend all logging messages with a configured string.

js
import { Server } from "@hocuspocus/server";
import { Logger } from "@hocuspocus/extension-logger";

const server = new Server({
  name: "hocuspocus-fra1-01",
  extensions: [new Logger()],
});

server.listen();

Disable messages

You can disable logging for specific messages.

js
import { Server } from "@hocuspocus/server";
import { Logger } from "@hocuspocus/extension-logger";

const server = new Server({
  extensions: [
    new Logger({
      onLoadDocument: false,
      onChange: false,
      onConnect: false,
      onDisconnect: false,
      onUpgrade: false,
      onRequest: false,
      onListen: false,
      onDestroy: false,
      onConfigure: false,
    }),
  ],
});

server.listen();

Custom logger

You can even pass a custom function to log messages.

js
import { Server } from "@hocuspocus/server";
import { Logger } from "@hocuspocus/extension-logger";

const server = new Server({
  extensions: [
    new Logger({
      log: (message) => {
        // do something custom here
        console.log(message);
      },
    }),
  ],
});

server.listen();

File: src/content/hocuspocus/server/extensions/overview.mdx


title: Server Extensions meta: title: Server Extensions | Hocuspocus Docs category: Hocuspocus

You can see our guide to custom extensions to find out how to create your own.

Table of Contents

We already created some very useful extensions you should check out for sure:

Extension Description
Database A generic database driver that is easily adjustable to work with any database.
Logger Add logging to Hocuspocus.
Redis Scale Hocuspocus horizontally with Redis.
SQLite Persist documents to SQLite.
Throttle Throttle connections by ips.
Webhook Send document changes via webhook to your API.
S3 Persist documents to Amazon S3 or any S3-compatible storage service.

File: src/content/hocuspocus/server/extensions/redis.mdx


title: Redis
meta:
title: Redis | Hocuspocus Docs
category: Hocuspocus
tags:


Hocuspocus can be scaled horizontally using the Redis extension. You can spawn multiple instances of the server behind a
load balancer and sync changes and awareness states through Redis. Hocuspocus will propagate all received updates to all other instances
using Redis and thus forward updates to all clients of all Hocuspocus instances.

The Redis extension does not persist data; it only syncs data between instances. Use the Database extension to store your documents.

Please note that all messages will be handled on all instances of Hocuspocus, so if you are trying to reduce cpu load by spawning multiple
servers, you should not connect them via Redis.

Thanks to @tommoor for writing the initial implementation of that extension.

Installation

Install the Redis extension with:

bash
npm install @hocuspocus/extension-redis

Configuration

For a full documentation on all available Redis and Redis cluster options, check out the
ioredis API docs.

js
import { Server } from "@hocuspocus/server";
import { Redis } from "@hocuspocus/extension-redis";

const server = new Server({
  extensions: [
    new Redis({
      // [required] Hostname of your Redis instance
      host: "127.0.0.1",

      // [required] Port of your Redis instance
      port: 6379,

      // [optional] Maximum time in milliseconds to wait for the current state
      // from another instance when loading a document. Defaults to 1000.
      awaitInitialSyncTimeout: 1000,
    }),
  ],
});

server.listen();

Consistent state on load

When an instance loads a document that is already open on another instance, the Redis extension waits for that instance to send its current state. This ensures that newly connected clients and direct connections receive changes that may not have been persisted yet. If no other instance has the document open, loading is not delayed.

The awaitInitialSyncTimeout option limits how long the extension waits, in milliseconds. It defaults to 1000. Set it to 0 to disable waiting and use fire-and-forget synchronization instead.

Usage

The Redis extension works well with the database extension. Once an instance stores a document, itโ€™s blocked for all
other instances to avoid write conflicts.

js
import { Server } from "@hocuspocus/server";
import { Logger } from "@hocuspocus/extension-logger";
import { Redis } from "@hocuspocus/extension-redis";
import { SQLite } from "@hocuspocus/extension-sqlite";

// Server 1
const server = new Server({
  name: "server-1", // make sure to use unique server names
  port: 1234,
  extensions: [
    new Logger(),
    new Redis({
      host: "127.0.0.1", // make sure to use the same Redis instance :-)
      port: 6379,
    }),
    new SQLite(),
  ],
});

server.listen();

// Server 2
const anotherServer = new Server({
  name: "server-2",
  port: 1235,
  extensions: [
    new Logger(),
    new Redis({
      host: "127.0.0.1",
      port: 6379,
    }),
    new SQLite(),
  ],
});

anotherServer.listen();

File: src/content/hocuspocus/server/extensions/s3.mdx


title: S3
meta:
title: S3 | Hocuspocus Docs
category: Hocuspocus
tags:


The S3 extension provides S3-compatible storage persistence for Hocuspocus. It supports Amazon S3 and S3-compatible services like MinIO, DigitalOcean Spaces, Wasabi, and more. Documents are stored as binary files in S3, making your collaborative documents persistent and scalable.

Installation

Install the S3 extension like this:

bash
npm install @hocuspocus/extension-s3

Configuration

bucket (required)

The S3 bucket name where documents will be stored. The bucket must exist and be accessible with your credentials.

region

AWS region for the S3 service.

Default: us-east-1

prefix

Key prefix for document storage. All documents will be stored under this prefix.

Default: hocuspocus-documents/

credentials

AWS credentials object containing accessKeyId and secretAccessKey. If not provided, the extension will use the default AWS credential chain (environment variables, IAM roles, etc.).

endpoint

Custom S3 endpoint URL for S3-compatible services like MinIO or DigitalOcean Spaces.

forcePathStyle

Use path-style URLs instead of virtual hosted-style URLs. Required for MinIO and some S3-compatible services.

Default: false

s3Client

Custom S3Client instance. If provided, other connection options will be ignored.

Usage

Basic AWS S3 Setup

js
import { Server } from "@hocuspocus/server";
import { S3 } from "@hocuspocus/extension-s3";

const server = new Server({
  extensions: [
    new S3({
      bucket: 'my-documents-bucket',
      region: 'us-east-1',
      credentials: {
        accessKeyId: 'your-access-key',
        secretAccessKey: 'your-secret-key'
      }
    }),
  ],
});

server.listen();

Using Environment Variables

js
import { Server } from "@hocuspocus/server";
import { S3 } from "@hocuspocus/extension-s3";

// Set environment variables:
// AWS_ACCESS_KEY_ID=your-access-key
// AWS_SECRET_ACCESS_KEY=your-secret-key
// AWS_REGION=us-west-2

const server = new Server({
  extensions: [
    new S3({
      bucket: 'my-documents-bucket',
      // Credentials will be loaded from environment variables
    }),
  ],
});

server.listen();

Using IAM Roles (EC2/Lambda)

js
import { Server } from "@hocuspocus/server";
import { S3 } from "@hocuspocus/extension-s3";

const server = new Server({
  extensions: [
    new S3({
      bucket: 'my-documents-bucket',
      region: 'us-east-1',
      // No credentials needed when using IAM roles
    }),
  ],
});

server.listen();

MinIO Configuration

For local development with MinIO, the extension automatically enables path-style URLs when an endpoint is provided:

js
import { Server } from "@hocuspocus/server";
import { S3 } from "@hocuspocus/extension-s3";

const server = new Server({
  extensions: [
    new S3({
      bucket: 'hocuspocus-documents',
      endpoint: 'http://localhost:9000',
      forcePathStyle: true, // Required for MinIO
      credentials: {
        accessKeyId: 'minioadmin',
        secretAccessKey: 'minioadmin'
      }
    }),
  ],
});

server.listen();

DigitalOcean Spaces

js
import { Server } from "@hocuspocus/server";
import { S3 } from "@hocuspocus/extension-s3";

const server = new Server({
  extensions: [
    new S3({
      bucket: 'my-spaces-bucket',
      region: 'nyc3',
      endpoint: 'https://nyc3.digitaloceanspaces.com',
      credentials: {
        accessKeyId: 'your-spaces-key',
        secretAccessKey: 'your-spaces-secret'
      }
    }),
  ],
});

server.listen();

Custom S3 Client

js
import { Server } from "@hocuspocus/server";
import { S3 } from "@hocuspocus/extension-s3";
import { S3Client } from "@aws-sdk/client-s3";

const customS3Client = new S3Client({
  region: 'eu-west-1',
  credentials: {
    accessKeyId: 'your-access-key',
    secretAccessKey: 'your-secret-key'
  },
  // Add any custom S3Client configuration here
});

const server = new Server({
  extensions: [
    new S3({
      bucket: 'my-documents-bucket',
      s3Client: customS3Client
    }),
  ],
});

server.listen();

Document Storage

Documents are stored as binary files in S3 with the following naming convention:

  • Key: {prefix}{documentName}.bin
  • Content-Type: application/octet-stream

For example, a document named "my-document" with the default prefix would be stored at:
hocuspocus-documents/my-document.bin

CLI Usage

You can also use the S3 extension with the Hocuspocus CLI:

bash
# Basic usage
hocuspocus --s3 --s3-bucket my-documents

# With custom region and prefix
hocuspocus --s3 --s3-bucket my-docs --s3-region eu-west-1 --s3-prefix "collab/"

# MinIO setup (forcePathStyle automatically enabled)
hocuspocus --s3 --s3-bucket hocuspocus-documents --s3-endpoint http://localhost:9000

# Using environment variables
export S3_BUCKET=my-documents
export AWS_ACCESS_KEY_ID=your-key
export AWS_SECRET_ACCESS_KEY=your-secret
hocuspocus --s3

CLI Environment Variables

  • AWS_ACCESS_KEY_ID - AWS access key ID
  • AWS_SECRET_ACCESS_KEY - AWS secret access key
  • AWS_REGION - AWS region
  • S3_BUCKET - S3 bucket name

IAM Permissions

Your AWS credentials or IAM role needs the following permissions for the specified bucket:

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:HeadObject"
      ],
      "Resource": "arn:aws:s3:::your-bucket-name/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:HeadBucket"
      ],
      "Resource": "arn:aws:s3:::your-bucket-name"
    }
  ]
}

Scaling with Redis

The S3 extension works seamlessly with the Redis extension for horizontal scaling. Redis handles real-time synchronization between server instances, while S3 provides persistent storage.

Basic S3 + Redis Setup

js
import { Server } from "@hocuspocus/server";
import { Logger } from "@hocuspocus/extension-logger";
import { Redis } from "@hocuspocus/extension-redis";
import { S3 } from "@hocuspocus/extension-s3";

// Server 1
const server1 = new Server({
  name: "server-1",
  port: 8001,
  extensions: [
    new Logger(),
    new Redis({
      host: "127.0.0.1",
      port: 6379,
    }),
    new S3({
      bucket: 'my-documents-bucket',
      region: 'us-east-1',
      credentials: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
      }
    }),
  ],
});

server1.listen();

// Server 2
const server2 = new Server({
  name: "server-2",
  port: 8002,
  extensions: [
    new Logger(),
    new Redis({
      host: "127.0.0.1",
      port: 6379,
    }),
    new S3({
      bucket: 'my-documents-bucket',
      region: 'us-east-1',
      credentials: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
      }
    }),
  ],
});

server2.listen();

MinIO + Redis for Local Development

js
import { Server } from "@hocuspocus/server";
import { Logger } from "@hocuspocus/extension-logger";
import { Redis } from "@hocuspocus/extension-redis";
import { S3 } from "@hocuspocus/extension-s3";

// Development server setup with MinIO and Redis
const createServer = (name, port) => new Server({
  name,
  port,
  extensions: [
    new Logger(),
    new Redis({
      host: "127.0.0.1",
      port: 6379,
    }),
    new S3({
      bucket: 'hocuspocus-documents',
      endpoint: 'http://localhost:9000',
      forcePathStyle: true,
      credentials: {
        accessKeyId: 'minioadmin',
        secretAccessKey: 'minioadmin'
      }
    }),
  ],
});

// Start multiple instances
createServer("dev-server-1", 8001).listen();
createServer("dev-server-2", 8002).listen();

Environment-based Configuration

js
import { Server } from "@hocuspocus/server";
import { Logger } from "@hocuspocus/extension-logger";
import { Redis } from "@hocuspocus/extension-redis";
import { S3 } from "@hocuspocus/extension-s3";

const server = new Server({
  name: process.env.SERVER_NAME || `server-${Math.random()}`,
  port: Number(process.env.PORT) || 8000,
  extensions: [
    new Logger(),
    new Redis({
      host: process.env.REDIS_HOST || "127.0.0.1",
      port: Number(process.env.REDIS_PORT) || 6379,
    }),
    new S3({
      bucket: process.env.S3_BUCKET,
      region: process.env.S3_REGION || 'us-east-1',
      endpoint: process.env.S3_ENDPOINT, // For MinIO
      forcePathStyle: process.env.S3_ENDPOINT ? true : false,
      prefix: process.env.S3_PREFIX || 'hocuspocus-documents/',
      credentials: process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY ? {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
      } : undefined
    }),
  ],
});

server.listen();

Development Environment Setup

Quick Start

For local development with MinIO (S3-compatible storage), use the built-in development scripts:

bash
# Set up complete development environment
npm run dev:setup

# Test S3 configuration
npm run dev:test-s3

# Start S3 playground examples
npm run playground:s3

Available Development Scripts

bash
# Environment setup
npm run dev:setup         # Complete setup (.env + Docker services)
npm run dev:env           # Create .env file from template
npm run dev:services      # Start Docker services (Redis + MinIO)
npm run dev:services:down # Stop Docker services
npm run dev:services:reset # Reset services and data

# S3 configuration testing
npm run dev:test-s3        # Test complete S3 configuration
npm run dev:test-s3:minio  # Test MinIO connection only
npm run dev:test-s3:nodejs # Test Node.js S3 client only

# Playground examples
npm run playground:s3      # S3 extension examples (ports 8000-8003)
npm run playground:s3-redis # S3 + Redis scaling examples

Local MinIO Setup

The development environment includes:

Default credentials for local development:

  • Access Key: minioadmin
  • Secret Key: minioadmin

Environment Variables

Create a .env file (automatically done by npm run dev:setup):

env
# MinIO S3 Configuration (for local development)
S3_ENDPOINT=http://localhost:9000
S3_BUCKET=hocuspocus-documents
S3_REGION=us-east-1
AWS_ACCESS_KEY_ID=minioadmin
AWS_SECRET_ACCESS_KEY=minioadmin

# Production AWS S3 Configuration
# S3_BUCKET=your-production-bucket
# S3_REGION=us-west-2
# AWS_ACCESS_KEY_ID=your-aws-access-key
# AWS_SECRET_ACCESS_KEY=your-aws-secret-key

Best Practices

Security

  • Use IAM roles instead of access keys when running on AWS infrastructure
  • Store credentials in environment variables, not in code
  • Follow the principle of least privilege for IAM permissions
  • Enable S3 bucket encryption for additional security

Performance

  • Use regions closest to your users for better latency
  • Consider S3 Transfer Acceleration for global applications
  • Monitor S3 request metrics and costs

Reliability

  • Enable S3 versioning to protect against accidental deletions
  • Set up appropriate S3 lifecycle policies for cost optimization
  • Consider cross-region replication for critical applications

Scaling Architecture

  • Redis: Handles real-time synchronization between server instances
  • S3: Provides persistent storage and acts as the source of truth
  • Load Balancer: Distributes connections across server instances
  • Multiple Regions: Deploy in multiple AWS regions for global performance

Troubleshooting

Common Issues

"Bucket not found" or access denied errors:

  • Verify the bucket name and region
  • Check IAM permissions
  • Ensure credentials are correctly configured

Connection timeouts with MinIO:

  • Make sure forcePathStyle: true is set
  • Verify the endpoint URL is accessible
  • Check firewall and network settings

Large document performance:

  • Monitor S3 transfer times
  • Consider using S3 multipart uploads for very large documents
  • Optimize document structure to reduce size

File: src/content/hocuspocus/server/extensions/sqlite.mdx


title: SQLite
meta:
title: SQLite | Hocuspocus Docs
category: Hocuspocus
tags:


Introduction

For local development purposes itโ€™s nice to have a database ready to go with a few lines of code. Thatโ€™s what the SQLite
extension is for.

Installation

Install the SQLite extension like this:

bash
npm install @hocuspocus/extension-sqlite better-sqlite3
npm install -D @types/better-sqlite3

Since v4, the extension uses better-sqlite3 instead of the unmaintained sqlite3 package. If you're upgrading from v3, uninstall the old dependency with npm uninstall sqlite3. Existing SQLite database files are fully compatible โ€” no data migration needed.

Configuration

database

Valid values are filenames, ":memory:" for an anonymous in-memory database and an empty
string for an anonymous disk-based database. Anonymous databases are not persisted and
when closing the database handle, their contents are lost.

https://github.com/mapbox/node-sqlite3/wiki/API#new-sqlite3databasefilename-mode-callback

Default: :memory:

schema

The SQLite schema thatโ€™s created for you.

Default:

sql
CREATE TABLE IF NOT EXISTS "documents" (
  "name" varchar(255) NOT NULL,
  "data" blob NOT NULL,
  UNIQUE(name)
)

If you wrote a custom schema under v3, note that v4's better-sqlite3 uses named parameters without the $ prefix โ€” rename $name to name and $data to data in your queries.

fetch

An async function to retrieve data from SQLite. If you change the schema, you probably want to override the query.

store

An async function to store data in SQLite. If you change the schema, you probably want to override the query.

Usage

By default, data is just โ€œstoredโ€ in :memory:, so itโ€™s wiped when you stop the server. You can pass a file name to
persist data on the disk.

js
import { Server } from "@hocuspocus/server";
import { SQLite } from "@hocuspocus/extension-sqlite";

const server = new Server({
  extensions: [
    new SQLite({
      database: "db.sqlite",
    }),
  ],
});

server.listen();

Troubleshooting

`Could not locate the bindings file`

better-sqlite3 ships prebuilt native bindings, but they don't always match fresh Node.js versions on day one. If the server throws Error: Could not locate the bindings file on start-up, rebuild the binding from source in your server project:

bash
npm rebuild better-sqlite3 --build-from-source

This compiles the native addon for your exact Node.js + OS + architecture combo. Requires a working C++ toolchain (Xcode Command Line Tools on macOS, build-essential on Debian/Ubuntu, or the Visual Studio Build Tools on Windows).


File: src/content/hocuspocus/server/extensions/throttle.mdx


title: Throttle
meta:
title: Throttle | Hocuspocus Docs
category: Hocuspocus
tags:


This extension throttles connection attempts and bans ip-addresses if it crosses the configured threshold.

Make sure to register it before any other extensions!

Installation

Install the Throttle package with:

bash
npm install @hocuspocus/extension-throttle

Configuration

js
import { Server } from "@hocuspocus/server";
import { Throttle } from "@hocuspocus/extension-throttle";

const server = new Server({
  extensions: [
    new Throttle({
      // [optional] allows up to 15 connection attempts per ip address per minute.
      // set to null or false to disable throttling, defaults to 15
      throttle: 15,

      // [optional] bans ip addresses for 5 minutes after reaching the threshold
      // defaults to 5
      banTime: 5,
    }),
  ],
});

server.listen();

File: src/content/hocuspocus/server/extensions/webhook.mdx


title: Webhooks
meta:
title: Webhooks | Hocuspocus Docs
category: Hocuspocus
tags:


The webhook extension allows you to connect Hocuspocus to your existing application by triggering webhooks on certain
events.

Installation

Install the Webhook package with:

bash
npm install @hocuspocus/extension-webhook

Configuration

js
import { Server } from "@hocuspocus/server";
import { Webhook, Events } from "@hocuspocus/extension-webhook";
import { TiptapTransformer } from "@hocuspocus/transformer";

const server = new Server({
  extensions: [
    new Webhook({
      // [required] url of your application
      url: "https://example.com/api/hocuspocus",

      // [required] a random string that will be used to verify the request signature
      secret: "459824aaffa928e05f5b1caec411ae5f",

      // [required] a transformer for your document
      transformer: TiptapTransformer,

      // [optional] array of events that will trigger a webhook
      // defaults to [ Events.onChange ]
      events: [Events.onConnect, Events.onCreate, Events.onChange, Events.onDisconnect],

      // [optional] time in ms the change event should be debounced,
      // defaults to 2000
      debounce: 2000,

      // [optional] time in ms after that the webhook will be sent
      // regardless of the configured debouncing, defaults to 10000
      debounceMaxWait: 10000,
    }),
  ],
});

server.listen();

How it works

The webhook extension listens on up to four configurable events/hooks that will trigger a POST request to the
configured url.

onConnect

When a new user connects to the server, the onConnect webhook will be triggered with the following payload:

json
{
  "event": "connect",
  "payload": {
    "documentName": "example-document",
    "requestHeaders": {
      "Example-Header": "Example"
    },
    "requestParameters": {
      "example": "12345"
    }
  }
}

You can respond with a JSON payload that will be set as context throughout the rest of the application. For example:

js
// authorize the user by the request parameters or headers
if (payload.requestParameters?.get("token") !== "secret-api-token") {
  response.writeHead(403, "unauthorized");
  return response.end();
}

// return context if authorized
response.writeHead(200, { "Content-Type": "application/json" });
response.end(
  JSON.stringify({
    user: {
      id: 1,
      name: "Jane Doe",
    },
  })
);

onCreate

When a new document is created the onCreate webhook will be triggered with the following payload:

json
{
  "event": "create",
  "payload": {
    "documentName": "example-document"
  }
}

You can use this to import a document into Hocuspocus. The webhook extension will first load the document from the
primary storage and only import it if it doesn't already exist in there.

Just respond with all the single documents keyed by their field name. For example:

js
response.writeHead(200, { "Content-Type": "application/json" });
response.end(
  JSON.stringify({
    // Document for the "secondary" field
    secondary: {},
    // Document for the "default" field
    default: {
      type: "doc",
      content: [
        {
          type: "paragraph",
          content: [
            {
              type: "text",
              text: "What is love?",
            },
          ],
        },
      ],
    },
  })
);

onChange

When a document is changed the onChange webhook will be triggered with the following payload including the context you
set before:

json
{
  "event": "change",
  "payload": {
    "documentName": "example-document",
    "document": {
      "another-field-name": {},
      "field-name": {
        "type": "doc",
        "content": [
          {
            "type": "paragraph",
            "content": [
              {
                "type": "text",
                "text": "What is love?"
              }
            ]
          }
        ]
      }
    },
    "context": {
      "user_id": 1,
      "name": "Jane Doe"
    }
  }
}

Because this happens on every keystroke up to multiple times a second, the webhook is debounced by default. You can
configure this (or shut it off entirely) with the debounce and debounceMaxWait configuration options.

onDisconnect

When a user disconnects the onDisconnect webhook will be triggered with the following payload:

json
{
  "event": "disconnect",
  "payload": {
    "documentName": "example-document",
    "context": {
      "user_id": 1,
      "name": "Jane Doe"
    }
  }
}

Transformation

The Y-Doc must be serialized into something readable by your application and when importing a document it must be
converted into a Y-Doc respectively.

Because Hocuspocus doesn't know how your data is structured, you need to pass a transformer to the Webhook extension.
You can use one of the transformers from the @hocuspocus/transformer package. Make sure to configure them properly.
In this example we used the TiptapTransformer that needs the list of extensions:

js
import { Server } from "@hocuspocus/server";
import { Webhook } from "@hocuspocus/extension-webhook";
import { TiptapTransformer } from "@hocuspocus/transformer";
import Document from "@tiptap/extension-document";
import Paragraph from "@tiptap/extension-paragraph";
import Text from "@tiptap/extension-text";

const server = new Server({
  extensions: [
    new Webhook({
      url: "https://example.com/api/webhook",
      secret: "459824aaffa928e05f5b1caec411ae5f",

      transformer: TiptapTransformer.extensions([Document, Paragraph, Text]),
    }),
  ],
});

server.listen();

Alternatively you can write your own implementation by simply passing functions that convert a Y-Doc to your
representation and vice versa:

js
import { Server } from "@hocuspocus/server";
import { Webhook } from "@hocuspocus/extension-webhook";
import { Doc } from "yjs";

const server = new Server({
  extensions: [
    new Webhook({
      url: "https://example.com/api/webhook",
      secret: "459824aaffa928e05f5b1caec411ae5f",

      transformer: {
        toYdoc(document: any, fieldName: string): Doc {
          // convert the given document (from your api) to a ydoc using the provided fieldName
          return new Doc();
        },
        fromYdoc(document: Doc): any {
          // convert the ydoc to your representation
          return document.toJSON();
        },
      },
    }),
  ],
});

server.listen();

Verify Request Signature

On your application server you should verify the signature coming from the webhook extension to secure the route.

The extension sends POST requests, and the signature is stored in the X-Hocuspocus-Signature-256 header containing a
message authentication code created with sha256.

Here are some examples how you could do that in different languages:

PHP

php
use Symfony\Component\HttpFoundation\Request;

function verifySignature(Request $request) {
  $secret = '459824aaffa928e05f5b1caec411ae5f';

  if (($signature = $request->headers->get('X-Hocuspocus-Signature-256')) == null) {
      throw new Exception('Header not set');
  }

  $parts = explode('=', $signature);

  if (count($parts) != 2) {
      throw new Exception('Invalid signature format');
  }

  $digest = hash_hmac('sha256', $request->getContent(), $secret);

  return hash_equals($digest, $parts[1]);
}

JavaScript

js
import { IncomingMessage } from 'http'

const secret = '459824aaffa928e05f5b1caec411ae5f'

const verifySignature = (request: IncomingMessage): boolean => {
  const signature = Buffer.from(request.headers['x-hocuspocus-signature-256'] as string)

  const hmac = createHmac('sha256', secret)
  const digest = Buffer.from(`sha256=${hmac.update(body).digest('hex')}`)

  return signature.length !== digest.length || timingSafeEqual(digest, signature)
}