LatteStream®

Quick Start

Getting Started

Building Custom SDKs

SDKs & Libraries

JavaScript/TypeScript

Node.js / Bun / Deno

Python

In Development

Go

In Development

PHP

In Development

API Reference

WebSocket API

REST API

Webhooks

Authentication

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.

Authentication Overview

LatteStream provides three levels of authentication:

  1. Connection Authentication - Required for all WebSocket connections
  2. Channel Authorization - Required for private and presence channels
  3. User Identification - Optional user metadata for presence channels

API Key Types

Public Keys (lspk_*)

Use Case: Client-side applications (browsers, mobile apps)

Characteristics:

  • Safe to expose in client-side code
  • Requires discovery service for node assignment
  • Requires auth endpoint for private/presence channels
  • Read-only access to public channels (can subscribe, but not trigger events)

Example:

const client = new LatteStream('lspk_1a2b3c4d5e6f...', { cluster: 'eu1', authEndpoint: 'https://your-app.com/auth', });

Private Tokens (lsk_*)

Format: lsk_{base64url(iv + encrypted_data + auth_tag)}

Use Case: Server-side applications only

Characteristics:

  • MUST be kept secret (never expose client-side)
  • Full access (can subscribe and trigger events)

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' });

Connection Authentication Flow

Public Key Flow (with Discovery Service)

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 } }

Private Token Flow (Direct)

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.

JWT Token Generation

Endpoint

POST /apps/token
Content-Type: application/json

Request

{ "api_key": "lsk_your_private_token", "socket_id": "user_123", "permissions": ["read", "write"], "expires_in": 1800 }

Parameters:

  • api_key (required): Your private API key
  • socket_id (required): Client identifier (e.g., user ID)
  • permissions (optional): Array of permissions
  • expires_in (optional): Token lifetime in seconds (max 86400 = 24 hours)

Response

{ "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", "token_type": "Bearer", "expires_in": 1800, "tenant_id": "123" }

Example Implementation

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', });

Channel Authorization

Private Channel Authorization

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 Channel Authorization

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" } } }

Security Best Practices

API Key Management

DO:

  • Store private keys in environment variables or secrets management
  • Rotate keys periodically
  • Use different keys for dev/staging/production
  • Monitor key usage for anomalies

DON'T:

  • Commit private keys to version control
  • Expose private keys in client-side code
  • Log private keys
  • Share keys between environments
  • Use test keys in production

Auth Endpoint Security

DO:

  • Always validate user sessions/tokens
  • Implement rate limiting
  • Use HTTPS only
  • Validate channel names against your business logic
  • Log authorization attempts
  • Return appropriate HTTP status codes (401, 403)

DON'T:

  • Auto-approve all auth requests
  • Trust client-provided data without validation
  • Expose internal error details
  • Allow unauthorized channel access

Channel Authorization Patterns

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); }

Error Handling

Connection Authentication Errors

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.

Channel Authorization Errors

Invalid auth token:

{ "event": "lattestream:error", "data": { "code": 4009, "message": "Unauthorized to access channel" } }

Client should:

  • Display error to user
  • Retry auth endpoint (may be transient error)
  • Log error for debugging

Integration Examples

React Hook

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 }; }

Express.js Auth Endpoint

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; }

Related Documentation

Support


Next Steps: Learn about Webhooks for server-side event notifications.