Stream Tokens
Streaming endpoints use a special authentication method because native video players (AVPlayer on Apple TV, ExoPlayer on Android TV) cannot set HTTP headers on individual segment requests.
How it works
Section titled “How it works”When a playback session is created, the client receives a session ID. Streaming URLs authenticate using the same Bearer token as the REST API, but support two methods:
Header-based (web)
Section titled “Header-based (web)”GET /api/stream/{sessionId}/master.m3u8Authorization: Bearer <token>The web client uses hls.js which can inject the Authorization header on every XHR request via the xhrSetup callback.
Query parameter (TV / mobile)
Section titled “Query parameter (TV / mobile)”GET /api/stream/{sessionId}/master.m3u8?token=<token>Native video players (AVPlayer, ExoPlayer) load HLS playlists and segments via their built-in HTTP stack, which doesn’t support custom headers. The token is passed as a query parameter instead.
Stream tokens vs Bearer tokens
Section titled “Stream tokens vs Bearer tokens”Stream tokens are not the same as session Bearer tokens. They are short-lived, HMAC-signed tokens scoped specifically to streaming:
| Property | Stream token | Bearer token |
|---|---|---|
| Format | userId:sessionId:expiresAt:hmac | Opaque session ID |
| Algorithm | HMAC-SHA256 | Database-backed session |
| Lifetime | 4 hours | 7 days (auto-extends) |
| Scope | Streaming endpoints only | All API endpoints |
| Comparison | crypto.timingSafeEqual | Database lookup |
| Key | BETTER_AUTH_SECRET | N/A |
Stream tokens are embedded in HLS playlist URLs so that native video players (which can’t set HTTP headers on segment requests) can authenticate. If a segment URL leaks, the blast radius is limited — the token expires in 4 hours and only grants access to streaming.
Security considerations
Section titled “Security considerations”- Limited scope — Stream tokens only authenticate streaming requests, not the full API. They cannot be used to modify data or access non-streaming endpoints.
- Session binding — Streaming sessions are bound to the user who created them. A valid token for a different user cannot access another user’s session.
Where tokens are used
Section titled “Where tokens are used”| Context | Auth method |
|---|---|
| REST API calls | Authorization: Bearer header |
| HLS playlist/segments (web) | Authorization: Bearer header via hls.js |
| HLS playlist/segments (TV) | ?token= query parameter |
| Trickplay sprite images | ?token= query parameter (needed for <img> tags) |
| SSE event stream (web) | Cookie (same-origin EventSource) |
| SSE event stream (TV) | ?token= query parameter |
| Image proxy | Optional (public images may not require auth) |
Custom player integration
Section titled “Custom player integration”If building a custom player that connects to Dubby:
- Authenticate —
POST /api/auth/sign-in/emailto get a Bearer token - Create session —
POST /v1/playback/sessionswith the token to get a session ID and master playlist URL - Load playlist — Fetch the master playlist URL, appending
?token=<token>if your player can’t set headers - Heartbeat — Send
POST /v1/playback/sessions/:id/heartbeatevery 10 seconds with the current position - End session —
DELETE /v1/playback/sessions/:idwhen playback stops
Sessions are automatically cleaned up after 2 minutes of no heartbeat.