Skip to content

SSE Backend Requirements — Admin App

The admin app frontend has been updated to use a single SSE stream per app: one organization-level endpoint that delivers both org-level and workspace-level events. This document describes what the backend needs to emit.

Architecture

Admin App (browser)

    │  GET /sse/organization/{orgSlug}/stream
    │  Authorization: Bearer {jwt}
    │  X-Client-Id: {uuid}

    └── Single SSE connection for the entire authenticated session
        ├── Organization-level events (workspace lifecycle, settings, subscription, members)
        └── Workspace-level events (status, settings, members, database, storage, keys, config)

The frontend connects when the user selects an organization and disconnects on org switch or logout. There is no separate workspace-level SSE connection in the admin app.

New Endpoint

GET /sse/organization/{orgSlug}/stream

  • Auth: Authorization: Bearer {jwt} header (NOT token-in-URL)
  • Headers: Accepts X-Client-Id header — echo back as originClientId in every event so the frontend can filter own events
  • Content-Type: text/event-stream
  • Scope: All events for the organization and all its workspaces

Event Envelope

Every event must follow this structure:

json
{
  "eventType": "organization.workspace.created",
  "organisationSlug": "my-org",
  "entityType": "Workspace",
  "entityId": "uuid-of-entity",
  "timestamp": "2026-03-07T12:00:00Z",
  "userId": "uuid-of-acting-user",
  "originClientId": "uuid-from-x-client-id-header",
  "data": {
    // Event-specific payload
  }
}

Event Types

Organization-Level Events

These are new — no existing BE implementation.

organization.workspace.created

Emitted when a workspace is created within the organization.

json
{
  "data": {
    "workspaceUuid": "string",
    "workspaceName": "string",
    "workspaceSlug": "string",
    "createdByEmail": "string"
  }
}

organization.workspace.deleted

Emitted when a workspace is deleted.

json
{
  "data": {
    "workspaceUuid": "string",
    "workspaceName": "string"
  }
}

organization.settings.updated

Emitted when organization name or slug changes.

json
{
  "data": {
    "name": "string (optional)",
    "slug": "string (optional)"
  }
}

organization.subscription.updated

Emitted when the subscription changes (e.g. Stripe webhook triggers plan change).

json
{
  "data": {
    "planName": "string",
    "status": "string"
  }
}

organization.member.created

Emitted when a member is added to the organization.

json
{
  "data": {
    "memberUuid": "string",
    "memberEmail": "string",
    "memberRole": "string",
    "memberStatus": "INVITED | ACTIVE | INACTIVE"
  }
}

organization.member.updated

Emitted when a member's role or status changes.

json
{
  "data": {
    "memberUuid": "string",
    "memberEmail": "string",
    "memberRole": "string",
    "memberStatus": "INVITED | ACTIVE | INACTIVE"
  }
}

organization.member.deleted

Emitted when a member is removed from the organization.

json
{
  "data": {
    "memberUuid": "string",
    "memberEmail": "string"
  }
}

Dashboard & Stats Events

These are aggregate events — they push fresh computed data so the FE can patch state without a round-trip.

dashboard.updated

Emitted when org-level usage or activity changes (workspace created/deleted, member added/removed, view created/deleted, storage changed). Contains the same data shape as GET /api/users/me dashboard fields.

json
{
  "eventType": "dashboard.updated",
  "organisationSlug": "acme-store",
  "entityType": "Dashboard",
  "operation": "UPDATE",
  "status": "COMPLETED",
  "data": {
    "usage": {
      "tierName": "PRO",
      "tierDisplayName": "Pro",
      "workspaceCount": 3,
      "maxWorkspaces": 10,
      "memberCount": 5,
      "maxMembers": 25,
      "totalStorageBytes": 104857600,
      "storageLimitBytes": 1073741824,
      "viewCount": 12
    },
    "recentActivity": [
      {
        "type": "SCHEMA_SYNC",
        "description": "Schema sync completed for 'products' view",
        "workspaceName": "My Workspace",
        "timestamp": "2026-03-13T14:30:00Z"
      }
    ]
  }
}

Trigger points: workspace create/delete, org member add/remove, view create/delete/update (sync), workspace member add/remove, file upload/delete (platform storage).

workspace.stats.updated

Emitted when per-workspace stats change. Delivers viewCount, memberCount, storageBytes, and lastActivity for a single workspace.

json
{
  "eventType": "workspace.stats.updated",
  "organisationSlug": "acme-store",
  "entityType": "Workspace",
  "entityId": "workspace-uuid",
  "workspaceId": "workspace-slug",
  "operation": "UPDATE",
  "status": "COMPLETED",
  "data": {
    "viewCount": 5,
    "memberCount": 3,
    "storageBytes": 52428800,
    "lastActivity": "2026-03-13T14:30:00Z"
  }
}

Trigger points: view create/delete/update (sync), workspace member add/remove, file upload/delete (platform storage).

Note: Both events are fire-and-forget — they never block the primary operation. They skip broadcasting if no SSE connections exist for the org (getConnectionCount == 0).

Workspace-Level Events (delivered via org stream)

These events are scoped to a specific workspace but delivered through the org-level stream. Some of these already exist on the workspace stream (/sse/workspace/{id}/stream) — they need to also be emitted on the org stream.

Already exist on workspace stream — also emit on org stream:

Event TypePayload (data)
workspace.status.changed{ previousStatus, newStatus }
workspace.settings.updated{ name?, description?, slug? }
workspace.member.added{ memberEmail, role }
workspace.member.removed{ membershipUuid, memberEmail }
workspace.member.role.changed{ membershipUuid, memberEmail, newRole }
workspace.database.configured{ databaseName, connectionStatus }
workspace.storage.configured{} (empty — FE reloads full config)

New workspace events — need BE to emit:

Event TypePayload (data)
workspace.apikey.created{ keyUuid, keyName, keyPrefix, createdAt }
workspace.apikey.rotated{ keyUuid, keyName, keyPrefix }
workspace.apikey.revoked{ keyUuid }
workspace.mcpkey.created{ keyUuid, keyName, keyPrefix, createdAt }
workspace.mcpkey.rotated{ keyUuid, keyName, keyPrefix }
workspace.mcpkey.revoked{ keyUuid }
workspace.mcp.configured{ accessMode }
workspace.api.configured{ maxExpandDepth, corsAllowedOrigins }

accessMode values: "DISABLED", "READ_ONLY", "READ_WRITE"

corsAllowedOrigins: string[] or null

Echo Filtering

The frontend sends X-Client-Id on connection. The backend must include this as originClientId in every event.

The frontend uses this to filter out its own events for config changes (to avoid overwriting mid-edit form state). Key create/revoke events are NOT filtered — the frontend handles idempotency in reducers.

Summary of BE Work

  1. New endpoint: GET /sse/organization/{orgSlug}/stream with JWT auth + X-Client-Id support
  2. Emit 7 org-level event types (workspace lifecycle, settings, subscription, members)
  3. Emit 2 aggregate event types (dashboard.updated, workspace.stats.updated) — ✅ implemented
  4. Emit 6 new workspace-level event types on the org stream (API keys, MCP keys, MCP config, API config)
  5. Mirror 7 existing workspace events onto the org stream (status, settings, members, database, storage)
  6. Include originClientId from X-Client-Id header in every event envelope

The existing workspace stream (/sse/workspace/{id}/stream) used by the data app remains unchanged.

SchemaStack Internal Developer Documentation