Skip to content

feat: add real-time invoice status updates via SSE (#537)#593

Open
goldandrew wants to merge 1 commit into
astera-hq:mainfrom
goldandrew:feat/sse-invoice-status-updates
Open

feat: add real-time invoice status updates via SSE (#537)#593
goldandrew wants to merge 1 commit into
astera-hq:mainfrom
goldandrew:feat/sse-invoice-status-updates

Conversation

@goldandrew

Copy link
Copy Markdown
Contributor

Summary

Closes #537

Replaces the polling-only model for contract event delivery with a true
Server-Sent Events (SSE) stream, so users see invoice status changes
within seconds instead of waiting for the next poll cycle.

  • frontend/app/api/events/route.ts — new GET /api/events SSE
    endpoint. Validates the wallet JWT (passed as ?token= query param
    because EventSource cannot send custom Authorization headers),
    polls the Soroban RPC every 3 s for new contract events from the
    invoice and pool contracts, optionally filters by invoiceId, and
    streams each event as a standard SSE message. A 30 s ping heartbeat
    prevents proxies from closing idle connections. The stream is cleaned
    up on client disconnect via request.signal.

  • frontend/lib/useContractEvents.ts — new useContractEvents
    hook. Opens a native EventSource to /api/events, invalidates the
    appropriate SWR cache keys (invoice, funded-invoice, position,
    token-totals) on every arriving event, and appends each event to the
    Zustand recentEvents list. EventSource reconnects automatically
    on drop; connection state is tracked via useState so callers can
    reflect live/offline status in the UI.

Acceptance criteria met

  • GET /api/events SSE endpoint implemented in Next.js API routes
  • Events streamed for invoice status changes, new fundings, and repayments
  • Frontend invalidates SWR cache on receiving relevant events
  • Connection automatically reconnects on drop (EventSource native behaviour)
  • SSE endpoint requires wallet authentication (JWT validation — unauthenticated requests get 401)

Test plan

  • Connect wallet → open a dashboard page → verify browser DevTools
    shows an open EventSource to /api/events
  • Trigger an invoice status change (e.g. fund an invoice in another
    tab) → confirm the counterparty dashboard refreshes within ~5 s
    without a manual page reload
  • Disconnect network → reconnect → confirm the stream re-establishes
    automatically
  • Attempt GET /api/events without a token → confirm 401 response
  • Attempt with an expired/invalid JWT → confirm 401 response

Implements GET /api/events as a true Server-Sent Events endpoint that
streams contract event notifications to authenticated clients in real
time, replacing the need to wait for the next polling cycle.

- frontend/app/api/events/route.ts: SSE endpoint that validates the
  wallet JWT (passed as ?token= since EventSource has no custom-header
  support), polls the Soroban RPC every 3 s for new contract events
  from the invoice and pool contracts, filters by invoiceId when
  supplied, and streams each event as an SSE message.  A 30 s heartbeat
  ping keeps proxies from closing idle connections.

- frontend/lib/useContractEvents.ts: React hook that opens an
  EventSource to /api/events, invalidates the relevant SWR cache keys
  (invoice, funded-invoice, position, token-totals) on each arriving
  event, and appends the event to the Zustand recent-events list.
  EventSource reconnects automatically on drop; the hook tracks live
  connection state via useState so callers can reflect it in UI.

Closes astera-hq#537
@sanmipaul

Copy link
Copy Markdown
Contributor

Good implementation overall, server-side polling, native EventSource reconnect, and SWR predicate invalidation are all the right choices. Three things need fixing before merge, plus one security concern to address or document.

Security

JWT in the query parameter leaks to server logs.
EventSource cannot send headers, so ?token=<jwt> is the only option — the PR documents this. The consequence is that the long-lived JWT appears permanently in Vercel/Nginx/CDN access logs, browser history, and Referer headers to third-party resources.

The standard mitigation is a short-lived one-time connection token: add a POST /api/events/token that validates the JWT via Authorization: Bearer and returns a single-use token valid for ~10s, then EventSource uses that. If that's out of scope for this PR, open a follow-up issue and note the log-scrubbing requirement (token= must be redacted from access logs) in the PR description.

Must fix before merge

onEvent in the useEffect dependency array reconnects the SSE stream on every parent render.
Inline arrow functions get a new reference each render, causing the effect to close and reopen EventSource constantly. Use a stable ref:

const onEventRef = useRef(onEvent);
useEffect(() => { onEventRef.current = onEvent; });
// In handler: onEventRef.current?.(data)
// Remove onEvent from deps: }, [enabled, invoiceId, walletAddress, setRecentEvents]);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add real-time invoice status updates via WebSocket or SSE

2 participants