In development · Early Access
Live CRUD over one WebSocket.
CrudWS is an open protocol for create, read, update and delete over a single persistent connection. Clients subscribe to the records they show, and the server pushes every change to all of them the moment it is stored. Any language, any framework, any server.
→ {"op":"hello","ref":"1","token":"eyJhbGciOi..."} ← {"op":"welcome","ref":"1","session":"s_8f2a"} → {"op":"subscribe","ref":"2","resource":"tasks"} ← {"op":"snapshot","ref":"2","items":[{"id":"t_7","version":4}],"cursor":"c_190"} → {"op":"update","ref":"3","resource":"tasks","id":"t_7","version":4,"data":{"done":true}} ← {"op":"ack","ref":"3","id":"t_7","version":5} ← {"op":"change","resource":"tasks","kind":"updated","id":"t_7","version":5}
how it works
From one socket to every screen in four steps
One connection carries everything: authentication, reads, writes and the changes other people make. Pick a step to highlight its frames in the diagram.
features
What the protocol defines, so you do not have to
Most teams rebuild these pieces on top of a raw WebSocket in every project. CrudWS writes them down once.
-
One connection for everything
Reads, writes and pushed changes share one persistent WebSocket. No second channel for updates, no request per poll.
-
Push to every subscriber
Change frames go to all clients subscribed to a resource, filtered for each client by what it may read.
-
Versions and conflicts
Every record has a version. Stale writes come back as a conflict frame with the current record, so optimistic updates stay safe.
-
Auth per connection
The client authenticates once in the handshake. The server binds the identity to the socket and ends the session when the token expires.
-
Permissions per resource
The server checks every subscribe, read and write against the identity, per resource and optionally per record.
-
Resume after reconnect
Each change carries a cursor. After a dropped connection the client subscribes again with its last cursor and receives what it missed.
protocol
A protocol you can read in one sitting
JSON text frames over WebSocket. Every frame has an op. Requests carry a ref that the server echoes in its answer, so answers may arrive in any order.
| op | direction | purpose |
|---|---|---|
| helloclient → server | client → server | Open the session: protocol version and credentials |
| subscribeclient → server | client → server | Receive changes for a resource, optionally filtered |
| unsubscribeclient → server | client → server | Stop a subscription |
| createclient → server | client → server | Insert a record |
| readclient → server | client → server | Fetch one record by id or a filtered list |
| updateclient → server | client → server | Change fields, with the version the client last saw |
| deleteclient → server | client → server | Remove a record, with its version |
| pingclient → server | client → server | Keep the connection alive |
| welcomeserver → client | server → client | Session accepted: id, heartbeat, granted permissions |
| snapshotserver → client | server → client | Current records and a cursor, answer to subscribe or read |
| ackserver → client | server → client | Write accepted, with the new version |
| changeserver → client | server → client | Pushed to subscribers: created, updated or deleted |
| conflictserver → client | server → client | Write rejected: stale version, current record attached |
| errorserver → client | server → client | Request failed: code and message |
| pongserver → client | server → client | Answer to ping |
// client to server→ {"op":"update","ref":"3","resource":"tasks","id":"t_7","version":4,"data":{"done":true}} // accepted← {"op":"ack","ref":"3","id":"t_7","version":5} // or: someone else saved first← {"op":"conflict","ref":"3","id":"t_7","version":6,"data":{"done":false,"title":"Ship v0"}}
- Envelope
- op, ref, resource, id, version, data and where. Short field names, nothing else required.
- Ordering
- Changes to one resource arrive in commit order on each connection.
- Error codes
- unauthorized, forbidden, not_found, invalid, rate_limited, unsupported, internal.
Draft v0, subject to change. Early Access members receive the full draft and help shape v1.
any stack
Works with any stack
CrudWS is a wire protocol, not a framework. Any language that can open a WebSocket and parse JSON can speak it, on the client and on the server. Here is the same session in four languages.
- JavaScript / TypeScript
- Python
- Go
- PHP
- Rust
- Java / Kotlin
- C# / .NET
- Ruby
- Elixir
- Swift
- Node.js
- Django
- Rails
- Laravel
- Phoenix
- ASP.NET
- Embedded / IoT
// Browser or Node.js 22+, built-in WebSocketconst ws = new WebSocket("wss://api.example.com/crud", "crudws.v0");const send = (frame) => ws.send(JSON.stringify(frame)); ws.onopen = () => { send({ op: "hello", ref: "1", token }); send({ op: "subscribe", ref: "2", resource: "tasks" });}; ws.onmessage = ({ data }) => { const frame = JSON.parse(data); if (frame.op === "change") { render(frame.id, frame.data, frame.version); }}; // later: mark task t_7 as done, based on version 4send({ op: "update", ref: "3", resource: "tasks", id: "t_7", version: 4, data: { done: true },});
# pip install websocketsimport asyncio, json, websockets async def main(): async with websockets.connect("wss://api.example.com/crud", subprotocols=["crudws.v0"]) as ws: await ws.send(json.dumps({"op": "hello", "ref": "1", "token": TOKEN})) await ws.send(json.dumps({"op": "subscribe", "ref": "2", "resource": "tasks"})) async for message in ws: frame = json.loads(message) if frame["op"] == "change": print(frame["kind"], frame["id"], frame["version"]) asyncio.run(main())
// go get github.com/coder/websocketconn, _, err := websocket.Dial(ctx, "wss://api.example.com/crud", &websocket.DialOptions{Subprotocols: []string{"crudws.v0"}})if err != nil { log.Fatal(err)}defer conn.CloseNow() wsjson.Write(ctx, conn, map[string]any{"op": "hello", "ref": "1", "token": token})wsjson.Write(ctx, conn, map[string]any{"op": "subscribe", "ref": "2", "resource": "tasks"}) for { var frame map[string]any if err := wsjson.Read(ctx, conn, &frame); err != nil { break } if frame["op"] == "change" { fmt.Println(frame["kind"], frame["id"], frame["version"]) }}
// composer require phrity/websocket$client = new WebSocket\Client("wss://api.example.com/crud");$client->addHeader("Sec-WebSocket-Protocol", "crudws.v0"); $client->text(json_encode(["op" => "hello", "ref" => "1", "token" => $token]));$client->text(json_encode(["op" => "subscribe", "ref" => "2", "resource" => "tasks"])); while ($message = $client->receive()) { $frame = json_decode($message->getContent(), true); if ($frame["op"] === "change") { echo "{$frame["kind"]} {$frame["id"]} v{$frame["version"]}\n"; }}
Illustrations of the raw protocol with a common WebSocket library per language. They are not official SDKs; there are none yet.
use cases
For screens that must never show stale data
-
Live dashboards
Orders, tickets, deployments, stock levels. Subscribe once and the numbers update themselves, without polling the API every few seconds.
← {"op":"change","resource":"orders", "kind":"created", "id":"o_1042", "version":1} -
Collaborative admin panels
Two people open the same customer record. Versions turn the second save into a visible conflict instead of a silent overwrite.
← {"op":"conflict","ref":"9", "resource":"customers", "id":"c_311", "version":8} -
Live lists and feeds
Kanban boards, activity feeds, queue views: rows appear, change and disappear on every open screen at the same time.
← {"op":"change","resource":"cards", "kind":"deleted", "id":"k_77", "version":12} -
IoT and device state
Devices write their state as small updates, dashboards and other devices subscribe. Permissions per resource keep each device to its own records.
→ {"op":"update","ref":"41", "resource":"devices", "id":"pump-3", "version":220, "data":{"rpm":1450}}
compare
How it compares
The alternatives are mature and widely used. CrudWS is a draft that does one thing: CRUD-shaped data with instant push, as an open protocol you can implement anywhere.
-
REST polling
- What it is
- HTTP API, asked again and again
- Writes
- Separate HTTP requests
- Changes reach clients
- On the next poll
- Concurrent edits
- Up to you (ETag and If-Match are possible)
- Backend
- Any
- Maturity
- Universal
-
GraphQL subscriptions
- What it is
- GraphQL operation type, streamed over WebSocket or SSE
- Writes
- Mutations, usually over HTTP
- Changes reach clients
- Per subscription defined in the schema
- Concurrent edits
- Up to you
- Backend
- Any GraphQL server
- Maturity
- Mature and widely deployed
-
Firebase / Supabase realtime
- What it is
- Managed backend platforms with realtime features
- Writes
- Through the vendor SDK or API
- Changes reach clients
- Pushed, built in
- Concurrent edits
- Transactions, otherwise last write wins (Firestore); your Postgres logic (Supabase)
- Backend
- Vendor platform (Supabase can be self-hosted)
- Maturity
- Production-grade, managed
-
CrudWS
- What it is
- Open protocol for CRUD and push over one WebSocket
- Writes
- Same socket, answered by request id
- Changes reach clients
- Pushed to every subscriber of the resource
- Concurrent edits
- Version on every write, conflict frame on mismatch
- Backend
- Any server that implements the spec
- Maturity
- In development, Early Access
Based on the commonly documented behaviour of each approach; details vary by implementation and configuration. If you need flexible queries across related data, GraphQL is the better fit.
faq
Questions
What is CrudWS?
An open protocol for real-time CRUD over WebSockets. It defines the JSON frames a client and a server exchange to create, read, update and delete records over one connection, and how the server pushes changes to every subscribed client.
Is it a library, a framework or a hosted service?
None of them. CrudWS is a protocol specification. You implement it on your own server, in your language, on your database. A reference implementation may follow; nothing is released yet.
Which languages and frameworks does it work with?
Any that can open a WebSocket and read JSON: JavaScript and TypeScript, Python, Go, PHP, Java, C#, Ruby, Elixir, Rust and others. Frameworks such as Django, Rails, Laravel or Phoenix are examples of servers that could implement it, not requirements.
How are conflicting edits handled?
Every record has a version. update and delete must carry the version the client last saw. If the record changed in the meantime, the server rejects the write with a conflict frame that contains the current record, and the client decides whether to merge, retry or show the conflict.
How is it different from GraphQL subscriptions?
GraphQL subscriptions stream the results of operations defined in a schema, and writes usually go over HTTP as mutations. CrudWS is narrower: records, versions and changes over one socket, without a query language. If you need flexible queries, GraphQL is the better fit.
Is CrudWS production ready?
No. The protocol is in development and the specification is a draft (v0). Please do not build production systems on it yet. Early Access is for teams who want to shape it.
What does Early Access include?
The full draft specification as soon as it is ready for review, a direct line to the people writing it, and early news about a reference implementation. Joining is free and commits you to nothing.
Who is behind CrudWS?
CrudWS is developed by MARCODES GmbH, a software company from Grünwald near Munich, Germany.
early access
Join Early Access
Get the draft specification first and help shape v1. We only write when there is something to review.
- The full v0 draft as soon as it is ready for review
- Your use case on the table while the protocol is still open
- Early news about a reference implementation