collaborative-text-editor
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCollaborative Text Editor
协作式文本编辑器
IMPORTANT: Before doing anything, you MUST read in this skill's directory. It contains essential guidance on debugging, error handling, state management, deployment, and project setup. Those rules and patterns apply to all RivetKit work. Everything below assumes you have already read and understood it.
BASE_SKILL.md重要提示:在进行任何操作之前,你必须阅读本技能目录下的。它包含了调试、错误处理、状态管理、部署和项目设置的关键指南。这些规则和模式适用于所有RivetKit工作。以下所有内容均假设你已阅读并理解该文档。
BASE_SKILL.mdWorking Examples
可用示例
If you need a reference implementation, read the raw working example code in these templates:
Patterns for building a Yjs server on RivetKit: CRDT document sync, presence and cursors, and snapshot persistence, with one Rivet Actor per document acting as a relay.
如果你需要参考实现,请查看以下模板中的完整工作示例代码:
基于RivetKit构建Yjs服务器的模式:CRDT文档同步、在线状态与光标、快照持久化,每个文档对应一个Rivet Actor作为中继。
Starter Code
起始代码
Start with the working example on GitHub and adapt it to your editor. It ships a React frontend with a plain textarea, remote cursor overlays, and a workspace document index.
| Use Case | Starter Code | Common Examples |
|---|---|---|
| Shared document editing | GitHub | Notion-style docs, shared notes, pair-writing tools, form co-editing |
从GitHub上的工作示例开始,根据你的编辑器需求进行适配。该示例包含一个React前端,带有纯文本输入框、远程光标覆盖层和工作区文档索引。
| 使用场景 | 起始代码 | 常见示例 |
|---|---|---|
| 共享文档编辑 | GitHub | Notion风格文档、共享笔记、结对写作工具、表单协同编辑 |
CRDT vs OT
CRDT vs OT
Two families of algorithms solve concurrent text editing. The choice decides what your server has to do.
| Dimension | CRDT (Yjs) | Operational Transformation |
|---|---|---|
| Conflict resolution model | Commutative merges. Updates apply in any order on any peer and converge to the same result. | Server transforms each operation against every concurrent operation. Correctness depends on a central sequencer. |
| Offline support | Strong. Clients keep editing locally and merge buffered updates on reconnect. | Weak. Long-lived divergence makes transformation chains complex and fragile. |
| Server role | Relay plus persistence. The server applies opaque updates and rebroadcasts them. It never needs to understand document semantics. | Authoritative transformer. The server must implement transformation logic for every operation type. |
| Library maturity | Yjs is mature and widely deployed, with bindings for ProseMirror, CodeMirror, Monaco, and others. | Production-grade implementations are mostly proprietary (Google Docs) or aging (ShareDB). |
The example uses Yjs because CRDTs let the server stay a relay-style Rivet Actor. The actor applies each incoming update to a server-side so it can persist the merged state and serve late joiners, but it never transforms operations or arbitrates conflicts. Ordering does not matter because Yjs merges are commutative.
Y.Doc有两类算法可解决并发文本编辑问题,选择哪种算法将决定服务器的职责。
| 维度 | CRDT (Yjs) | 操作转换(OT) |
|---|---|---|
| 冲突解决模型 | 可交换合并。更新可在任意节点以任意顺序应用,最终收敛至相同结果。 | 服务器针对每个并发操作转换操作内容。正确性依赖于中央排序器。 |
| 离线支持 | 强支持。客户端可在本地继续编辑,重新连接时合并缓存的更新。 | 弱支持。长时间的分歧会使转换链变得复杂且脆弱。 |
| 服务器角色 | 中继加持久化。服务器应用不透明的更新并重新广播,无需理解文档语义。 | 权威转换器。服务器必须为每种操作类型实现转换逻辑。 |
| 库成熟度 | Yjs成熟且广泛部署,支持ProseMirror、CodeMirror、Monaco等多种编辑器绑定。 | 生产级实现大多为专有(如Google Docs)或已过时(如ShareDB)。 |
示例使用Yjs是因为CRDT允许服务器作为中继式Rivet Actor运行。Actor将每个传入的更新应用到服务器端的,以便持久化合并后的状态并为后期加入的客户端提供服务,但它从不转换操作或仲裁冲突。由于Yjs的合并是可交换的,顺序无关紧要。
Y.DocDocument Actor Model
文档Actor模型
| Topic | Summary |
|---|---|
| Topology | One |
| Sync model | Each client holds a local |
| Persistence | Full merged Yjs snapshot overwritten in one binary actor KV key ( |
| Queues | None. The example is purely actions plus broadcast events. |
| Presence | Yjs Awareness relayed through the same |
The two-actor split follows the coordinator pattern from Design Patterns: the coordinator owns discovery and creation, and each document actor owns one document's realtime state. Multi-part keys scope both actors to a workspace.
Actors
-
Key:
document[workspaceId, documentId] -
Responsibility: Applies incoming sync and awareness updates to a server-sideand
Y.Doc, persists the merged Yjs snapshot to actor KV, and broadcasts updates to all connected collaborators.Awareness -
Actions
getContentapplyUpdategetAwareness
-
Queues
- None
-
State
- JSON metadata only: ,
title,createdAtupdatedAt - Binary KV key holding the full merged Yjs snapshot
yjs:doc - Ephemeral vars: the live and
Y.Doc, created inAwarenessand rehydrated from KV on actor startcreateVars - Per-connection :
connStateof awareness clients asserted by that connectionclientIds
- JSON metadata only:
-
Key:
documentList[workspaceId] -
Responsibility: Coordinator for one workspace. Creates document actors through the actor-to-actor client and maintains the index of document summaries.
-
Actions
createDocumentlistDocumentsdeleteDocument
-
Queues
- None
-
State
- JSON
- array of
documentsentries (DocumentSummary,id,title,createdAt)updatedAt
The coordinator's generates a UUID, then explicitly creates the document actor with and passes as creation input, which the document actor's consumes. See Communicating Between Actors for the actor-to-actor client.
createDocumentc.client<typeof registry>(){ title, createdAt }createState| 主题 | 摘要 |
|---|---|
| 拓扑结构 | 每个文档对应一个 |
| 同步模型 | 每个客户端持有本地 |
| 持久化 | 每次同步更新时,将完整的合并Yjs快照覆盖写入一个二进制Actor KV键( |
| 队列 | 无。示例仅使用动作加广播事件。 |
| 在线状态 | Yjs Awareness通过相同的 |
Actors
-
键:
document[workspaceId, documentId] -
职责: 将传入的同步和感知更新应用到服务器端的和
Y.Doc,将合并后的Yjs快照持久化到Actor KV,并向所有连接的协作者广播更新。Awareness -
动作
getContentapplyUpdategetAwareness
-
队列
- 无
-
状态
- 仅JSON元数据:、
title、createdAtupdatedAt - 二进制KV键,存储完整的合并Yjs快照
yjs:doc - 临时变量:实时的和
Y.Doc,在Awareness中创建,并在Actor启动时从KV恢复createVars - 每个连接的:该连接断言的Awareness客户端ID列表
connStateclientIds
- 仅JSON元数据:
-
键:
documentList[workspaceId] -
职责: 单个工作区的协调器。通过Actor-to-Actor客户端创建文档Actor,并维护文档摘要索引。
-
动作
createDocumentlistDocumentsdeleteDocument
-
队列
- 无
-
状态
- JSON格式
- 数组,包含
documents条目(DocumentSummary、id、title、createdAt)updatedAt
Update Relay
更新中继
A single action handles both update kinds. Updates cross the action boundary as byte arrays and are converted back to on each side.
applyUpdate(update, kind, clientId?)number[]Uint8Array| Kind | Server Applies To | Persists | Broadcasts |
|---|---|---|---|
| | Full merged snapshot to KV key | |
| | Nothing. Presence is ephemeral. | |
Note the asymmetry on the sync branch: the broadcast carries only the small incremental update, while the KV write stores the full merged document re-encoded with .
Y.encodeStateAsUpdateYjs origin tags are the echo guards that keep the relay loop-free:
| Origin Tag | Set Where | Effect |
|---|---|---|
| Client edits inside | The client's update listener fires and sends |
| Server applying an incoming update to its | Marks the change as client-originated on the server copy. |
| Client applying broadcast events or initial sync data | Update listeners early-return on |
On connect or reconnect, the client calls and , then applies both results to its local and with origin . After that, every change flows through and the broadcast events.
getContentgetAwarenessY.DocAwareness"remote"applyUpdate单个动作处理两种类型的更新。更新以字节数组的形式跨动作边界传递,并在两端转换回。
applyUpdate(update, kind, clientId?)number[]Uint8Array| 类型 | 服务器应用到 | 是否持久化 | 是否广播 |
|---|---|---|---|
| 通过 | 将完整合并快照写入KV键 | 携带增量更新的 |
| 通过 | 不持久化。在线状态是临时的。 | 携带更新的 |
注意同步分支的不对称性:广播仅携带小的增量更新,而KV写入存储的是通过重新编码的完整合并文档。
Y.encodeStateAsUpdateYjs来源标记是防止中继循环的回声防护:
| 来源标记 | 设置位置 | 作用 |
|---|---|---|
| 客户端在 | 客户端的更新监听器触发,并向Actor发送 |
| 服务器将传入更新应用到其 | 在服务器副本上标记该变更为客户端发起。 |
| 客户端应用广播事件或初始同步数据时 | 更新监听器遇到 |
连接或重新连接时,客户端调用和,然后将结果应用到本地和,来源标记为。之后,所有变更都通过和广播事件流转。
getContentgetAwarenessY.DocAwareness"remote"applyUpdateAwareness And Presence
感知与在线状态
Presence (user names, colors, cursor positions) rides on the Yjs Awareness protocol instead of actor state:
- Clients set presence with for the
awareness.setLocalStateFieldanduserfields. The awareness update listener encodes the change and sendscursor.applyUpdate(update, "awareness", awareness.clientID) - The actor records each asserted in that connection's
clientId, applies the update to the server-sideconnState.clientIds, and broadcasts theAwarenessevent to all peers. See Connections for per-connection state.awareness - reads the connection's
onDisconnect, callsclientIdson the server-sideremoveAwarenessStates, and broadcasts the encoded removal so every remaining client drops the departed user's cursor. See Lifecycle for the hook.Awareness
Because the actor tracks which awareness clientIds belong to which connection, presence cleanup is automatic on disconnect with no client cooperation required.
在线状态(用户名、颜色、光标位置)基于Yjs Awareness协议传递,而非Actor状态:
- 客户端使用设置
awareness.setLocalStateField和user字段的在线状态。感知更新监听器对变更进行编码,并发送cursor。applyUpdate(update, "awareness", awareness.clientID) - Actor在该连接的中记录每个已断言的
connState.clientIds,将更新应用到服务器端的clientId,并向所有对等方广播Awareness事件。有关每个连接状态的详细信息,请参阅连接。awareness - 读取连接的
onDisconnect,在服务器端的clientIds上调用Awareness,并广播编码后的移除操作,以便所有剩余客户端移除已离开用户的光标。有关钩子的详细信息,请参阅生命周期。removeAwarenessStates
由于Actor跟踪哪些Awareness客户端ID属于哪个连接,断开连接时无需客户端配合即可自动清理在线状态。
Persistence And Compaction
持久化与压缩
The example persists with a full-snapshot overwrite: on every update, the actor re-encodes the entire merged document with and overwrites the single binary KV key . There is no append-only update log and no separate compaction job. Compaction is implicit because emits one compact merged representation of the document, so Yjs merge semantics keep the stored blob compact on their own.
"sync"Y.encodeStateAsUpdateyjs:docY.encodeStateAsUpdate| Property | Full-Snapshot Overwrite (the example) |
|---|---|
| Write cost | One full-document KV write per sync update, so every keystroke rewrites the whole blob. |
| Read cost | One binary KV read in |
| Crash safety | The last completed |
| Sweet spot | Small to medium documents where simplicity beats write amplification. |
Recommended extension (not in the example): for large documents or very high edit rates, switch to appending incremental updates to a KV update log and writing a merged snapshot only periodically (for example every N updates). Boot becomes snapshot plus log replay, and the snapshot write becomes the explicit compaction step that truncates the log. Adopt this only when full-snapshot writes become the measured bottleneck or the blob approaches KV value size limits.
示例使用完整快照覆盖的方式持久化:每次更新时,Actor通过重新编码整个合并文档,并覆盖单个二进制KV键。没有追加式更新日志,也没有单独的压缩任务。压缩是隐式的,因为会生成文档的一个紧凑合并表示,因此Yjs的合并语义会自动保持存储的Blob紧凑。
"sync"Y.encodeStateAsUpdateyjs:docY.encodeStateAsUpdate| 属性 | 完整快照覆盖(示例采用) |
|---|---|
| 写入成本 | 每次同步更新执行一次完整文档KV写入,因此每次按键都会重写整个Blob。 |
| 读取成本 | 在 |
| 崩溃安全性 | 最后完成的 |
| 适用场景 | 中小型文档,其中简洁性优于写入放大。 |
推荐扩展(示例未实现):对于大型文档或极高编辑频率,切换为将增量更新追加到KV更新日志,并仅定期写入合并快照(例如每N次更新)。启动时需要快照加日志重放,快照写入成为截断日志的显式压缩步骤。仅当完整快照写入成为实测瓶颈或Blob接近KV值大小限制时才采用此方案。
Lifecycle
生命周期
mermaid
sequenceDiagram
participant A as Client A
participant B as Client B
participant DL as documentList
participant D as document
A->>DL: listDocuments()
A->>DL: createDocument(title)
DL->>D: create([workspaceId, documentId], input)
DL-->>A: DocumentSummary
A->>D: connect
B->>D: connect
Note over D: createVars rehydrates Y.Doc from KV "yjs:doc"
A->>D: getContent() + getAwareness()
D-->>A: encoded doc + awareness state
Note over A: local edit with origin "local"
A->>D: applyUpdate(update, "sync")
Note over D: apply with origin "client", overwrite KV snapshot
D-->>B: sync event (incremental update)
Note over B: apply with origin "remote", no echo
A->>D: applyUpdate(update, "awareness", clientId)
D-->>B: awareness event
B-->>D: disconnect
Note over D: onDisconnect removes B's awareness clientIds
D-->>A: awareness event (removal)mermaid
sequenceDiagram
participant A as Client A
participant B as Client B
participant DL as documentList
participant D as document
A->>DL: listDocuments()
A->>DL: createDocument(title)
DL->>D: create([workspaceId, documentId], input)
DL-->>A: DocumentSummary
A->>D: connect
B->>D: connect
Note over D: createVars rehydrates Y.Doc from KV "yjs:doc"
A->>D: getContent() + getAwareness()
D-->>A: encoded doc + awareness state
Note over A: local edit with origin "local"
A->>D: applyUpdate(update, "sync")
Note over D: apply with origin "client", overwrite KV snapshot
D-->>B: sync event (incremental update)
Note over B: apply with origin "remote", no echo
A->>D: applyUpdate(update, "awareness", clientId)
D-->>B: awareness event
B-->>D: disconnect
Note over D: onDisconnect removes B's awareness clientIds
D-->>A: awareness event (removal)Security Checklist
安全检查清单
The example ships with no authentication or authorization. Harden it with this baseline before production. None of these are implemented in the example.
- Authenticate before connect: Anyone who knows or guesses a workspace ID can connect, and because implicitly getOrCreates, connecting with a nonexistent workspace ID silently creates a blank
useActorcoordinator. Add connection auth so unauthenticated clients never reach an actor. See Authentication.documentList - Per-document access control: Validate that the authenticated user is allowed to access the specific key, not just any document.
[workspaceId, documentId] - Cap and rate limit : Update payloads are unvalidated
applyUpdatearrays with no size limit, and the example client sends one action per keystroke and per cursor move with zero throttling. Enforce payload size caps and per-connection rate limits on the server, and debounce on the client.number[] - Do not trust client-asserted awareness clientIds: The argument to
clientIdis client-supplied and trusted as-is. Derive or verify presence identity from connection-scoped server state instead.applyUpdate - Destroy actors and KV on delete: only filters the entry out of the coordinator's index. The document actor and its KV snapshot are orphaned. On delete, also destroy the document actor and its storage, with a permission check on who may delete.
deleteDocument
示例未包含任何认证或授权机制。在投入生产前,请通过以下基线进行加固。这些内容均未在示例中实现。
- 连接前认证:任何知道或猜测到工作区ID的人都可以连接,并且由于隐式执行getOrCreates,使用不存在的工作区ID连接会静默创建一个空白的
useActor协调器。添加连接认证,使未认证客户端无法访问Actor。请参阅认证。documentList - 每个文档的访问控制:验证已认证用户是否有权访问特定的键,而非任意文档。
[workspaceId, documentId] - 限制并速率限制:更新负载是未验证的
applyUpdate数组,没有大小限制,示例客户端每次按键和光标移动都会发送一个动作,且没有任何节流。在服务器上实施负载大小限制和每个连接的速率限制,并在客户端上进行防抖处理。number[] - 不要信任客户端断言的Awareness客户端ID:的
applyUpdate参数由客户端提供并被直接信任。应从连接范围的服务器状态派生或验证在线状态身份。clientId - 删除时销毁Actor和KV:仅从协调器的索引中过滤掉条目。文档Actor及其KV快照会成为孤儿。删除时,还应销毁文档Actor及其存储,并检查谁有权限执行删除操作。
deleteDocument
Reference Map
参考映射
Actors
Actors
- Access Control
- Actions
- Actor Keys
- Actor Scheduling
- Actor Statuses
- AI and User-Generated Rivet Actors
- Authentication
- Communicating Between Actors
- Connections
- Custom Inspector Tabs
- Debugging
- Design Patterns
- Destroying Actors
- Errors
- Fetch and WebSocket Handler
- Helper Types
- Icons & Names
- Input Parameters
- Lifecycle
- Limits
- Low-Level HTTP Request Handler
- Low-Level KV Storage
- Low-Level WebSocket Handler
- Metadata
- Next.js Quickstart
- Node.js & Bun Quickstart
- Queues & Run Loops
- React Quickstart
- Realtime
- Rust Quickstart (Preview)
- Sandbox Actor
- Scaling & Concurrency
- Sharing and Joining State
- SQLite
- SQLite + Drizzle
- State & Storage
- Testing
- Troubleshooting
- Types
- Vanilla HTTP API
- Versions & Upgrades
- Workflows
- Access Control
- Actions
- Actor Keys
- Actor Scheduling
- Actor Statuses
- AI and User-Generated Rivet Actors
- Authentication
- Communicating Between Actors
- Connections
- Custom Inspector Tabs
- Debugging
- Design Patterns
- Destroying Actors
- Errors
- Fetch and WebSocket Handler
- Helper Types
- Icons & Names
- Input Parameters
- Lifecycle
- Limits
- Low-Level HTTP Request Handler
- Low-Level KV Storage
- Low-Level WebSocket Handler
- Metadata
- Next.js Quickstart
- Node.js & Bun Quickstart
- Queues & Run Loops
- React Quickstart
- Realtime
- Rust Quickstart (Preview)
- Sandbox Actor
- Scaling & Concurrency
- Sharing and Joining State
- SQLite
- SQLite + Drizzle
- State & Storage
- Testing
- Troubleshooting
- Types
- Vanilla HTTP API
- Versions & Upgrades
- Workflows
Agent Os
Agent Os
- Agent-to-Agent Communication
- agentOS vs Sandbox
- Authentication
- Benchmarks
- Configuration
- Core Package
- Cron Jobs
- Deployment
- Embedded LLM Gateway
- Events
- Filesystem
- Limitations
- LLM Credentials
- Multiplayer
- Networking & Previews
- Overview
- Permissions
- Persistence & Sleep
- Pi
- Processes & Shell
- Queues
- Quickstart
- Sandbox Mounting
- Security & Auth
- Security Model
- Sessions
- Software
- SQLite
- System Prompt
- Tools
- Webhooks
- Workflow Automation
- Agent-to-Agent Communication
- agentOS vs Sandbox
- Authentication
- Benchmarks
- Configuration
- Core Package
- Cron Jobs
- Deployment
- Embedded LLM Gateway
- Events
- Filesystem
- Limitations
- LLM Credentials
- Multiplayer
- Networking & Previews
- Overview
- Permissions
- Persistence & Sleep
- Pi
- Processes & Shell
- Queues
- Quickstart
- Sandbox Mounting
- Security & Auth
- Security Model
- Sessions
- Software
- SQLite
- System Prompt
- Tools
- Webhooks
- Workflow Automation
Clients
Clients
- Node.js & Bun
- React
- Swift
- SwiftUI
- Node.js & Bun
- React
- Swift
- SwiftUI
Connect
Connect
- Deploy To Amazon Web Services Lambda
- Deploying to AWS ECS
- Deploying to Cloudflare Workers
- Deploying to Freestyle
- Deploying to Google Cloud Run
- Deploying to Hetzner
- Deploying to Kubernetes
- Deploying to Railway
- Deploying to Rivet Compute
- Deploying to Supabase Functions
- Deploying to Vercel
- Deploying to VMs & Bare Metal
- Deploy To Amazon Web Services Lambda
- Deploying to AWS ECS
- Deploying to Cloudflare Workers
- Deploying to Freestyle
- Deploying to Google Cloud Run
- Deploying to Hetzner
- Deploying to Kubernetes
- Deploying to Railway
- Deploying to Rivet Compute
- Deploying to Supabase Functions
- Deploying to Vercel
- Deploying to VMs & Bare Metal
Cookbook
Cookbook
- AI Agent
- AI Agent Workspaces
- Chat Room
- Collaborative Text Editor
- Cron Jobs and Scheduled Tasks
- Database per Tenant
- Deploying Rivet in a VPC or Air-Gapped Network
- Live Cursors and Presence
- Multiplayer Game
- AI Agent
- AI Agent Workspaces
- Chat Room
- Collaborative Text Editor
- Cron Jobs and Scheduled Tasks
- Database per Tenant
- Deploying Rivet in a VPC or Air-Gapped Network
- Live Cursors and Presence
- Multiplayer Game
General
General
- Actor Configuration
- Architecture
- Cross-Origin Resource Sharing
- Documentation for LLMs & AI
- Edge Networking
- Endpoints
- Environment Variables
- HTTP Server
- Logging
- Pool Configuration
- Production Checklist
- Registry Configuration
- Runtime Modes
- Actor Configuration
- Architecture
- Cross-Origin Resource Sharing
- Documentation for LLMs & AI
- Edge Networking
- Endpoints
- Environment Variables
- HTTP Server
- Logging
- Pool Configuration
- Production Checklist
- Registry Configuration
- Runtime Modes
Self Hosting
Self Hosting
- Configuration
- Docker Compose
- Docker Container
- File System
- FoundationDB (Enterprise)
- Installing Rivet Engine
- Kubernetes
- Multi-Region
- PostgreSQL
- Production Checklist
- Railway Deployment
- Render Deployment
- TLS & Certificates
- Configuration
- Docker Compose
- Docker Container
- File System
- FoundationDB (Enterprise)
- Installing Rivet Engine
- Kubernetes
- Multi-Region
- PostgreSQL
- Production Checklist
- Railway Deployment
- Render Deployment
- TLS & Certificates