agentleFS
Sign inSign up

golid / rules

golid-ai/golid/.cursor/rules/sse-realtime.mdc

SSE and real-time event patterns — hub, ticket auth, keepalive, reconnect

Cursor rule40 starsChanged 4 months ago
---
description: SSE and real-time event patterns — hub, ticket auth, keepalive, reconnect
globs: backend/internal/service/sse/**/*.go,backend/internal/handler/sse*.go,frontend/src/lib/sse.ts
alwaysApply: false
---

# SSE / Real-Time Patterns

> **Thesis:** SSE uses one-time ticket auth (never JWT in URLs), per-user buffered channels, and frontend reconnect with exponential backoff.

## Hub Architecture

`SSEHub` is a singleton created in `wire.BuildServices` (`internal/wire/services.go`). Per-user channel sets with buffered channels (16). Non-blocking sends — if a client's buffer is full, the event is dropped (logged as warning). Max 5 connections per user prevents resource exhaustion.

```go
// in wire.BuildServices:
sseHub := sse.NewSSEHub(cfg.SSETicketTTL)
// Pass to handler and any service that needs to push events
```

`main.go` shuts down the hub after HTTP drain: `svcs.SSEHub.Shutdown()`.

## Ticket Auth (not JWT in URL)

`EventSource` can't set `Authorization` headers. Never put JWTs in URLs (they leak into logs). Use one-time tickets:

1. Client calls `POST /api/v1/events/ticket` with JWT auth → gets a 30s single-use ticket
2. Client opens `EventSource("/api/v1/events/stream?ticket=<ticket>")`
3. Server validates and burns the ticket, begins streaming

The stream endpoint is on the public API group (not behind JWT middleware) — it uses ticket auth directly.

## Keepalive

Send `: keepalive\n\n` every 30 seconds. Cloud Run closes idle connections at 15 minutes.

## Frontend Reconnect

On disconnect: refresh access token first (it may have expired), request a new ticket, reconnect with exponential backoff (1s → 2s → 4s → max 30s). Reset backoff on successful connect.

## Scaling Limitation

The hub is in-memory — all connected clients must be on the same instance. For multi-instance deployments, add a pub/sub layer (Redis, NATS). `Send()` and `Broadcast()` are the integration points.

## Adding New Event Types

Backend — publish from any service:

```go
sseHub.Send(userID, sse.SSEEvent{
    Event: "order_updated",
    Data:  map[string]any{"order_id": orderID, "status": newStatus},
})
```

Frontend — register a typed handler:

```typescript
onSSEEvent<OrderUpdate>("order_updated", (data) => {
    toast.info(`Order ${data.order_id} is now ${data.status}`);
});
```

## Middleware

SSE endpoints must be excluded from gzip and timeout middleware (long-lived connections). Both are handled via path checks in `middleware/stack.go` for `/api/v1/events/stream`.

Discussion

Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.

Posts are public.Sign in to post

No one has posted yet. Be the first.