Skip to main content

Overview

DeployStack Backend includes @fastify/sse for Server-Sent Events support. SSE provides a simple, unidirectional communication channel from server to client over HTTP - ideal for live updates, notifications, and streaming data. The plugin is globally registered with a 30-second heartbeat interval to keep connections alive.

Naming Convention Standard

All SSE endpoints MUST follow this URL pattern:
  • REST endpoint: /api/{resource}/{action}
  • SSE endpoint: /api/{resource}/{action}/stream

URL Pattern Examples

Paired Endpoints

Every SSE endpoint should have a corresponding REST endpoint:
  1. Same Query Parameters: Both endpoints accept identical query parameters
  2. Same Data Structure: Both return the same data format
  3. Consistent Behavior: Both apply the same filters and limits
  4. Fallback Support: REST endpoint serves as fallback for clients without SSE support

Why /stream?

  • Industry Standard: Used by GitHub, Stripe, and Twitter APIs
  • RESTful: Treats streaming as a sub-resource
  • Technology Agnostic: Works for SSE, WebSockets, or any streaming protocol
  • Clear Intent: Immediately indicates real-time streaming capability

Enabling SSE on a Route

Add the sse: true option to any route definition:
For route-specific configuration:

Sending Messages

Single Message

Full SSE Format

Streaming with Async Generator

Connection Management

Keep Connection Open

By default, the connection closes after the handler completes. To keep it open:

Handle Disconnection

Manual Close

Client Reconnection

Handle reconnecting clients using the Last-Event-ID header:
Access the last event ID directly:

Connection State

Complete Route Example

Polling Pattern with Async Operations

When using setInterval with async database queries or API calls, you must check connection state after the async operation completes to prevent crashes.

Critical: Timestamp Tracking with Array Mutations

⚠️ COMMON BUG: When polling for new records and reversing arrays for chronological order, you must capture the newest timestamp before reversing the array:
Why this matters:
  • Query returns items in descending order (newest first): [newest, ..., oldest]
  • array.reverse() mutates the array: [oldest, ..., newest]
  • After reversal, array[0] points to the oldest item
  • Using array[0].created_at after reversal sets lastSentTimestamp to the oldest timestamp
  • Next poll finds the same items again → infinite duplicate stream
The Fix: Always capture the newest timestamp from array[0] before calling reverse().

Complete Polling Pattern Example

Why Multiple Checks Are Needed

Without the second check after async operations:
With proper checks:
The client can disconnect during async operations, so checking only at the start of the interval is insufficient.

Key Takeaways for Polling Patterns

When implementing SSE with polling:
  1. ✅ Always check reply.sse.isConnected after async operations - Client can disconnect during queries
  2. ✅ Capture timestamps before array mutations - array.reverse() mutates the array
  3. ✅ Use gt() (greater than) for timestamp filtering - Prevents re-sending same items
  4. ✅ Clear interval on disconnect - Prevent memory leaks and unnecessary queries
  5. ✅ Send in chronological order - Oldest first for natural reading experience
  6. ✅ Wrap polling logic in try-catch - Don’t crash on database errors
Common Pitfalls:
  • ❌ Using lastSentTimestamp = array[0] after array.reverse()
  • ❌ Only checking isConnected before async operations
  • ❌ Forgetting to clear interval in onClose handler
  • ❌ Not handling database errors gracefully

Frontend Client

TypeScript Types

Import types from the package: