Driftstack DRIFTSTACK docs
Docs

Account notifications

A single Server-Sent Events stream for every notification scoped to the calling account. Use it to power a “what’s happening on my account right now” panel without N poll-loops.

The stream is a v0 surface (2026-05-20) and is read-only — it doesn’t replace the durable audit log or the email channel. It’s an additive, low-latency notification path.

Stream notifications

GET /v1/account/me/notifications

Auth: bearer token via Authorization: Bearer <token> header OR ?ds_token=<token> query-string fallback. The browser EventSource API can’t set custom headers, so the query-string fallback exists for that case (the same contract as the transcript stream); the header still wins when both are supplied. Server-side runtimes that can set headers (Tauri’s invoke bridge, Node’s eventsource package, etc.) should prefer the header.

The token must carry the broad read scope; account_owner also satisfies the gate. Resource-granular scopes such as read:sessions, read:webhooks, or read:audit deliberately do not, because this one stream mixes cost telemetry, audit, incident, and session events. Treat both the token and the resulting stream as sensitive account-wide data.

Frame shape

Each event in the stream uses the SSE event: header as a discriminator and the data: line as a JSON-encoded payload.

event: cost.threshold_alert
data: {"kind":"cost.threshold_alert","accountId":"acc_...","severity":"warn",…}

event: session.errored
data: {"kind":"session.errored","accountId":"acc_...","errorClass":"driver_error",…}

Subscribers can either:

  • Use one EventSource.addEventListener('cost.threshold_alert', …) per kind (recommended — the native router does the dispatch), or
  • Subscribe via EventSource.onmessage and switch on payload.kind from the parsed JSON.

A : heartbeat <ISO8601>\n\n comment frame fires every ~25 seconds to keep load-balancers from closing idle connections. Ignore these.

Event kinds (v0)

cost.threshold_alert

Fires when the account’s operational cost-to-serve estimate crosses an operator threshold (soft or hard, in either direction). This is a unit-economics signal for product operations. It is not a customer spending cap, an invoice event, or an overage trigger, and it does not email, rate-limit, or interrupt the account.

field type notes
kind "cost.threshold_alert" discriminator
accountId string calling account
severity "warn" | "critical" | "resolved" resolved = estimate dropped below soft
billingCycle string YYYY-MM UTC
previousState "under-soft" | "between-soft-and-hard" | "over-hard" | null null on first-ever evaluation
currentState same enum
totalCents number operational estimate for the current cycle
thresholdSoftCents number operator soft threshold
thresholdHardCents number operator hard threshold
at string ISO8601 server publish time

incident.broadcast

Fires when a public incident is posted, updated, or resolved. (Live since 2026-07-07 — this kind was previously declared in the type union with no publisher; the status-page incident lifecycle now publishes it.) Incident frames are a platform-wide broadcast: every account with an open stream receives the same incident, stamped with its own accountId. The frame carries the incident’s current severity and title — for full detail (affected components, update history, resolution state), query the status endpoints.

field type notes
kind "incident.broadcast"
accountId string calling account
incidentId string inc_…
severity "minor" | "major" | "outage"
title string short headline
at string ISO8601 server publish time

audit.high_severity

Selective republish from the audit log for high-severity actions (e.g. api_key.revoked, account.byok_anthropic_key_set, team.member_removed). Low-severity events stay in the audit log only — query GET /v1/account/audit-log for the full ledger.

field type notes
kind "audit.high_severity"
accountId string
action string canonical audit action enum
actorType "customer" | "admin" | "system"
targetResourceId string | null e.g. key_…
at string ISO8601

session.errored

Fires when a session’s driver hits a terminal error before the customer-initiated destroy.

field type notes
kind "session.errored"
accountId string
sessionId string ses_…
errorClass string e.g. driver_error, timeout
at string ISO8601

Reconnect + persistence

The bus is in-memory only — events with no live subscribers are dropped on the floor. EventSource’s native auto-reconnect (default 3s backoff) is the v0.1 reconnect story; transient drops resume without any app-level glue.

There is no Last-Event-ID resume: a reconnect resumes the live stream without replaying missed frames. For the durable trail of any event covered by audit.high_severity, query GET /v1/account/audit-log.

Quotas + rate-limit

The SSE route permits up to 10 concurrent subscribers per account and shares the same global rate-limit bucket as every other authenticated read. An additional connection receives 429 with Retry-After: 30. One stream per running app instance is normally sufficient because every subscriber sees every event.

Customer example (TypeScript)

The stream is plain SSE — no SDK helper is needed. In a browser (or any runtime with EventSource), pass the token via ?ds_token= since EventSource can’t set headers:

const es = new EventSource(
  `https://api.driftstack.dev/v1/account/me/notifications?ds_token=${KEY}`,
);

es.addEventListener('cost.threshold_alert', (e) => {
  const event = JSON.parse(e.data);
  showToast(`Cost ${event.severity}: $${event.totalCents / 100}`);
});

es.addEventListener('session.errored', (e) => {
  const event = JSON.parse(e.data);
  showBanner(`Session ${event.sessionId} errored: ${event.errorClass}`);
});

// later, on app teardown:
es.close();

Server-side runtimes that can set headers should send Authorization: Bearer <token> instead of the query-string token.

Full design notes live at docs/internal/driftstack-telemetry-event-schema-for-gui-panel.md in the source repo.