Real-time collaborative sync for local-first applications
Kraken provides WebSocket-based real-time sync for Yjs documents. Each app gets isolated namespaces for data separation.
Before connecting, your app must be registered with Kraken (contact admin or use the API):
POST /api/apps
Authorization: Bearer YOUR_ADMIN_TOKEN
Content-Type: application/json
{
"app_id": "my-app",
"name": "My Application",
"allowed_origins": ["https://my-app.example.com"],
"owner_email": "dev@example.com"
}
const ws = new WebSocket(
'wss://kraken.helixpods.ai/doc/my-document?token=YOUR_TOKEN'
);
Kraken uses a two-layer security model:
A shared secret that proves your app is authorized to use Kraken. Pass it as:
?token=YOUR_TOKENAuthorization: Bearer YOUR_TOKENYour app's origin (domain) determines which namespace your rooms belong to.
This is automatic - Kraken reads the Origin header from WebSocket connections.
Each registered app gets isolated data:
| Your Request | Internal Room ID |
|---|---|
/doc/readme from app-a.example.com |
app-a:readme |
/doc/readme from app-b.example.com |
app-b:readme |
Health check endpoint. No authentication required.
curl https://kraken.helixpods.ai/health
WebSocket endpoint for Yjs sync. Requires authentication.
wss://kraken.helixpods.ai/doc/my-room?token=TOKEN
List registered apps. Admin authentication required.
Register a new app. Admin authentication required.
{
"app_id": "string (required)",
"name": "string (required)",
"allowed_origins": ["array of strings (required)"],
"description": "string (optional)",
"owner_email": "string (optional)"
}
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
const doc = new Y.Doc();
const provider = new WebsocketProvider(
'wss://kraken.helixpods.ai',
'my-document',
doc,
{ params: { token: 'YOUR_TOKEN' } }
);
provider.on('status', ({ status }) => {
console.log('Connection status:', status);
});
// Use your Yjs document
const text = doc.getText('content');
text.insert(0, 'Hello, collaborative world!');
import * as Y from 'yjs';
import * as syncProtocol from 'y-protocols/sync';
import * as encoding from 'lib0/encoding';
import * as decoding from 'lib0/decoding';
const doc = new Y.Doc();
const ws = new WebSocket('wss://kraken.helixpods.ai/doc/my-room?token=YOUR_TOKEN');
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
// Send sync step 1
const encoder = encoding.createEncoder();
encoding.writeVarUint(encoder, 0); // message type: sync
syncProtocol.writeSyncStep1(encoder, doc);
ws.send(encoding.toUint8Array(encoder));
};
ws.onmessage = (event) => {
const decoder = decoding.createDecoder(new Uint8Array(event.data));
const messageType = decoding.readVarUint(decoder);
// Handle sync messages...
};
import { useEffect, useState } from 'react';
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
function useYjsDocument(roomName: string) {
const [doc] = useState(() => new Y.Doc());
const [connected, setConnected] = useState(false);
useEffect(() => {
const provider = new WebsocketProvider(
'wss://kraken.helixpods.ai',
roomName,
doc,
{ params: { token: process.env.KRAKEN_TOKEN } }
);
provider.on('status', ({ status }) => {
setConnected(status === 'connected');
});
return () => provider.destroy();
}, [roomName, doc]);
return { doc, connected };
}
Kraken supports WebRTC signaling for peer-to-peer video, audio, and data channels. The signaling protocol runs alongside Yjs sync on the same WebSocket connection.
1. Announce - When you connect, announce yourself to get the peer list
2. Discover - Receive list of peers and notifications when peers join/leave
3. Signal - Exchange SDP offers/answers and ICE candidates through Kraken
4. Connect - Establish direct P2P connection for media/data
Signaling messages are JSON wrapped in an envelope:
{
"kraken": "signal",
"signal": {
"type": 0, // SignalMessageType enum
"from": "peer-uuid",
"to": "target-peer-uuid", // optional for broadcasts
...
}
}
| Type | Value | Description |
|---|---|---|
| Announce | 0 | Register yourself and get peer list |
| Offer | 1 | Send SDP offer to initiate call |
| Answer | 2 | Send SDP answer to accept call |
| IceCandidate | 3 | Exchange ICE candidates |
| Hangup | 4 | End a call |
| PeerList | 5 | List of all peers (server โ client) |
| PeerJoined | 6 | New peer notification |
| PeerLeft | 7 | Peer disconnected notification |
import { KrakenClient } from '@kraken/client';
const client = new KrakenClient({
url: 'wss://kraken.helixpods.ai',
token: 'YOUR_TOKEN',
room: 'my-room',
name: 'Alice',
});
// Listen for events
client.on((event) => {
switch (event.type) {
case 'peers':
console.log('Peers in room:', event.peers);
break;
case 'peer-joined':
console.log('New peer:', event.peer.name);
break;
case 'call-incoming':
console.log('Incoming call from:', event.from.name);
break;
case 'call-established':
// Attach remote stream to video element
videoEl.srcObject = event.remoteStream;
break;
}
});
await client.connect();
// Start a video call
await client.call('peer-id', { video: true, audio: true });
import { useKraken, useVideoCall } from '@kraken/client/react';
function VideoRoom() {
const { peers, signaling } = useKraken({
url: 'wss://kraken.helixpods.ai',
token: process.env.KRAKEN_TOKEN,
room: 'video-room',
name: 'Alice',
});
const { localStream, remoteStreams, call, hangup } = useVideoCall({
signaling,
video: true,
audio: true,
});
return (
<div>
{/* Local video */}
<video ref={el => el && (el.srcObject = localStream)} autoPlay muted />
{/* Remote videos */}
{Array.from(remoteStreams).map(([peerId, stream]) => (
<video key={peerId} ref={el => el && (el.srcObject = stream)} autoPlay />
))}
{/* Peer list */}
{peers.map(peer => (
<button key={peer.peerId} onClick={() => call(peer.peerId)}>
Call {peer.name}
</button>
))}
</div>
);
}
By default, Kraken client uses Google's public STUN servers. For production, configure your own:
const client = new KrakenClient({
url: 'wss://kraken.helixpods.ai',
token: 'YOUR_TOKEN',
room: 'my-room',
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:turn.example.com:3478',
username: 'user',
credential: 'pass'
}
]
});
| Code | Meaning | Solution |
|---|---|---|
| 401 | Unauthorized | Check your token |
| 403 | Forbidden | Origin not registered |
| 426 | Upgrade Required | Use WebSocket, not HTTP |
โ Back to Home ยท GitHub ยท Nexartis