Docs / Realtime WebSocket v1
Realtime WebSocket v1
Use the legacy public-room connection and its message and history formats.
Separate public demos from private features
The v1 endpoint is /_joripspace/realtime. It publishes and subscribes by room, but it does not provide v2 server-signed access tokens or per-user authorization inside a room. An unguessable room name is not access control.
Use the sample main room only for a public demo. Use the v2 guide for orders, private notifications and member-only conversations. The two versions keep rooms and data separate.
Send and receive in two browser windows
// For public demos only. Use v2 for member-only messages.
// Add <button id="send" disabled>Send</button><ul id="messages"></ul> to the page
const room = 'main';
const url = new URL('/_joripspace/realtime', location.origin);
url.protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
url.searchParams.set('room', room);
url.searchParams.set('resource_type', 'chat');
const socket = new WebSocket(url);
const button = document.querySelector('#send');
const seen = new Set();
function render(message) {
if (message.type !== 'chat.message' || !message.id || seen.has(message.id)) return;
seen.add(message.id);
const item = document.createElement('li');
item.textContent = String(message.data?.body || '');
document.querySelector('#messages').append(item);
}
socket.addEventListener('message', async (event) => {
const message = JSON.parse(event.data);
if (message.type === 'system.connected') {
button.disabled = false;
// Attach the receive listener first, then merge history and realtime events by ID.
const historyUrl = new URL('/_joripspace/realtime/rooms/' + room + '/messages', location.origin);
historyUrl.searchParams.set('resource_type', 'chat');
historyUrl.searchParams.set('limit', '50');
const response = await fetch(historyUrl);
if (!response.ok) return console.warn('Could not load message history.');
const page = await response.json();
page.messages.forEach(render);
// Keep next_cursor and pass it as before to load older history.
} else if (message.type === 'error') console.warn(message.code);
else render(message);
});
button.addEventListener('click', () => {
if (socket.readyState !== WebSocket.OPEN) return;
const message = { id: crypto.randomUUID(), type: 'chat.message', data: { body: 'Hello!' } };
socket.send(JSON.stringify(message)); // reuse the same message object when retrying
});
socket.addEventListener('close', () => { button.disabled = true; });
window.addEventListener('pagehide', () => socket.close());Connect two separate windows to the same project, room and resource_type. Enable sending only after receiving the system connection event. Writing a database row or keeping sockets in a Worker-global Map does not publish to a platform room.
History and missed-message recovery
const url = new URL('/_joripspace/realtime/rooms/main/messages', location.origin);
url.searchParams.set('resource_type', 'chat');
url.searchParams.set('limit', '50');
if (nextCursor) url.searchParams.set('before', nextCursor);
url.searchParams.set('search', 'hello');
const response = await fetch(url);
if (!response.ok) throw new Error('History request failed');
const page = await response.json();A production v1 client needs exponential reconnect backoff with jitter, multi-page history recovery, ID-based deduplication and stable time-and-ID sorting. Bound the displayed list and deduplication set. Use v2 when you need an automatic recovery SDK.
List, edit and delete messages
History is newest first. Reverse copied pages for oldest-first display, merge pages and live events by ID, then sort by created_at and ID. Reset the cursor when the search changes and prevent stale requests from overwriting newer results.
v1 has no API that mutates an existing message body. Store the source and version in the app database, then publish a new change event. Use v2 plus app-server authorization when only authors or administrators may edit or delete.
// For public v1 rooms only. room and messageId come from the current list.
const url = new URL('/_joripspace/realtime/rooms/' + encodeURIComponent(room)
+ '/messages/' + encodeURIComponent(messageId), location.origin);
url.searchParams.set('resource_type', 'chat');
const response = await fetch(url, { method: 'DELETE' });
if (!response.ok && response.status !== 404) throw new Error('Delete failed');
// Remove the row when deletion succeeds or the message is already absent.
// v1 does not automatically send deletion events to other screens.
// Other screens must reload history and replace the list in the current query range.After deletion, compare only the currently fetched history range. A message missing from one page may still exist on another page.
Verify actual delivery
Clearing the sender input is not proof of delivery. Confirm the same message ID in two windows, isolation between rooms and history recovery for messages published during a disconnect. Never place tokens, email addresses or personal data in a WebSocket URL.