LatteStream®
Quick Start
Getting Started
Building Custom SDKs
SDKs & Libraries
JavaScript/TypeScript
Node.js / Bun / Deno
Python
Go
PHP
API Reference
WebSocket API
REST API
Webhooks
Authentication
Complete guide to LatteStream authentication and authorization
LatteStream uses a multi-layered authentication system to secure your real-time applications. This guide covers API keys, token generation, channel authorization, and security best practices.
LatteStream provides three levels of authentication:
lspk_*)Use Case: Client-side applications (browsers, mobile apps)
Characteristics:
Example:
const client = new LatteStream('lspk_1a2b3c4d5e6f...', { cluster: 'eu1', authEndpoint: 'https://your-app.com/auth', });
lsk_*)Format: lsk_{base64url(iv + encrypted_data + auth_tag)}
Use Case: Server-side applications only
Characteristics:
Security Warning: Never include private tokens in client-side code, version control, or logs.
Example (server-side only):
const server = new LatteStreamServer('lsk_9z8y7x6w5v4u...'); await server.trigger('my-channel', 'event', { data: 'value' });
Step 1: Discovery Request
GET /discover?api_key=lspk_1a2b3c4d5e6f... Host: eu1.lattestream.com
Response:
{ "node_id": "node-1", "region": "eu1", "cluster": "eu1", "host": "node-1.eu1.lattestream.com", "port": 443, "discovery_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_at": "2024-01-01T12:00:00Z" }
Step 2: WebSocket Connection
// Connect to assigned node with discovery token const ws = new WebSocket(`wss://${host}?discovery_token=${discovery_token}`);
Step 3: Authenticate
ws.onopen = () => { ws.send( JSON.stringify({ api_key: discovery_token, }) ); };
Step 4: Connection Established
{ "event": "lattestream:connection_established", "data": { "socket_id": "123.456", "activity_timeout": 120, "protocol": 7 } }
Step 1: WebSocket Connection
const ws = new WebSocket('wss://eu1.lattestream.com');
Step 2: Authenticate
ws.onopen = () => { ws.send( JSON.stringify({ api_key: 'lsk_your_private_token', }) ); };
Step 3: Connection Established
Same as public key flow.
POST /apps/token
Content-Type: application/json
{ "api_key": "lsk_your_private_token", "socket_id": "user_123", "permissions": ["read", "write"], "expires_in": 1800 }
Parameters:
api_key (required): Your private API keysocket_id (required): Client identifier (e.g., user ID)permissions (optional): Array of permissionsexpires_in (optional): Token lifetime in seconds (max 86400 = 24 hours){ "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", "token_type": "Bearer", "expires_in": 1800, "tenant_id": "123" }
Server-side (Node.js):
const { LatteStreamServer } = require('@lattestream/server'); const server = new LatteStreamServer('lsk_your_private_token'); app.post('/api/get-token', async (req, res) => { const userId = req.user.id; // From your auth system const response = await fetch('https://eu1.lattestream.com/apps/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ api_key: 'lsk_your_private_token', socket_id: userId, expires_in: 3600, // 1 hour }), }); const { access_token } = await response.json(); res.json({ token: access_token }); });
Client-side:
// 1. Get token from your server const response = await fetch('/api/get-token'); const { token } = await response.json(); // 2. Connect with token const client = new LatteStream(token, { cluster: 'eu1', });
Private channels (private-*) require server-side authorization to prevent unauthorized access.
Authorization Flow:
1. Client attempts to subscribe to "private-chat"
2. Client SDK calls authEndpoint with socket_id and channel_name
3. Your server validates user's identity/permissions
4. Server generates auth token using LatteStream SDK
5. Server returns auth token to client
6. Client includes auth token in subscribe message
Example Implementation:
Client-side:
const client = new LatteStream('lspk_public_key', { authEndpoint: 'https://your-app.com/auth', }); // Client SDK automatically calls authEndpoint when subscribing const channel = client.subscribe('private-user-123');
Server-side:
const { LatteStreamServer } = require('@lattestream/server'); const server = new LatteStreamServer('lsk_your_private_token'); app.post('/auth', (req, res) => { const { socket_id, channel_name } = req.body; // 1. Validate user's session/token const user = req.session.user; if (!user) { return res.status(401).json({ error: 'Unauthorized' }); } // 2. Check if user can access this channel if (channel_name === `private-user-${user.id}`) { // User can only access their own private channel const auth = server.authorizeChannel(socket_id, channel_name); return res.json(auth); } res.status(403).json({ error: 'Forbidden' }); });
Auth Response Format:
{ "auth": "lspc_base64url_encrypted_token" }
Presence channels (presence-*) require authorization PLUS user identification.
Example Implementation:
app.post('/auth', (req, res) => { const { socket_id, channel_name } = req.body; const user = req.session.user; if (!user) { return res.status(401).json({ error: 'Unauthorized' }); } // Authorize with user data const auth = server.authorizeChannel(socket_id, channel_name, { user_id: user.id, user_info: { name: user.name, avatar: user.avatar, status: 'online', }, }); res.json(auth); });
Auth Response Format:
{ "auth": "lspc_base64url_encrypted_token", "channel_data": { "user_id": "user123", "user_info": { "name": "Alice", "avatar": "https://...", "status": "online" } } }
DO:
DON'T:
DO:
DON'T:
User-specific channels:
// Only allow users to access their own private channel if (channel_name === `private-user-${user.id}`) { return server.authorizeChannel(socket_id, channel_name); }
Team/organization channels:
// Check if user is member of the team const teamId = channel_name.split('-')[1]; // "private-team-123" if (await isTeamMember(user.id, teamId)) { return server.authorizeChannel(socket_id, channel_name); }
Role-based access:
// Check if user has permission if ( user.role === 'admin' || channel_name.startsWith(`private-user-${user.id}`) ) { return server.authorizeChannel(socket_id, channel_name); }
Invalid API Key:
WebSocket closes with code and reason. No explicit error message sent.
Recommended: Handle ws.onerror and ws.onclose events, then retry with exponential backoff.
Invalid auth token:
{ "event": "lattestream:error", "data": { "code": 4009, "message": "Unauthorized to access channel" } }
Client should:
import { useEffect, useState } from 'react'; import { LatteStream } from '@lattestream/client'; export function useAuth() { const [token, setToken] = useState<string | null>(null); const [client, setClient] = useState<LatteStream | null>(null); useEffect(() => { async function authenticate() { // Get JWT token from your backend const response = await fetch('/api/get-token'); const { token } = await response.json(); setToken(token); // Create LatteStream client const lattestream = new LatteStream(token, { cluster: 'eu1', }); lattestream.connect(); setClient(lattestream); } authenticate(); return () => { client?.disconnect(); }; }, []); return { client, token }; }
const express = require('express'); const session = require('express-session'); const { LatteStreamServer } = require('@lattestream/server'); const app = express(); app.use(express.json()); app.use( session({ secret: 'your-secret', resave: false, saveUninitialized: false }) ); const server = new LatteStreamServer('lsk_your_private_token'); // Middleware to require authentication function requireAuth(req, res, next) { if (!req.session.user) { return res.status(401).json({ error: 'Unauthorized' }); } next(); } // Auth endpoint for private/presence channels app.post('/auth', requireAuth, (req, res) => { const { socket_id, channel_name } = req.body; const user = req.session.user; // Validate channel access if (!canAccessChannel(user, channel_name)) { return res.status(403).json({ error: 'Forbidden' }); } // For presence channels, include user info if (channel_name.startsWith('presence-')) { const auth = server.authorizeChannel(socket_id, channel_name, { user_id: user.id, user_info: { name: user.name, avatar: user.avatar, }, }); return res.json(auth); } // For private channels const auth = server.authorizeChannel(socket_id, channel_name); res.json(auth); }); function canAccessChannel(user, channelName) { // Implement your authorization logic if (channelName.startsWith('private-user-')) { const userId = channelName.split('-')[2]; return user.id === userId; } return false; }
Next Steps: Learn about Webhooks for server-side event notifications.