# How Instatic Broadcasts Plugin Lifecycle Events via SSE: Real-Time Plugin State Management

> Discover how Instatic broadcasts plugin lifecycle events via SSE for real-time admin tab updates. Learn about its reliable server-sent events pipeline.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-30

---

**Instatic uses a Server-Sent Events (SSE) pipeline to push real-time plugin lifecycle notifications—such as installed, updated, crashed, restarted, enabled, and disabled—from the server to every open admin tab, utilizing lazy client-side connections and server-side heartbeats for reliability.**

The CoreBunch/Instatic repository implements a robust SSE-based event system to notify admin interfaces about plugin state changes without polling. This architecture ensures that multiple browser tabs maintain synchronized views of plugin installation, updates, crashes, and configuration changes. Understanding how Instatic's event system broadcasts plugin lifecycle events via SSE reveals a design pattern that balances real-time responsiveness with connection efficiency.

## Server-Side SSE Stream Implementation

### Route Handler and ReadableStream Setup

The server-side entry point resides in [`server/handlers/cms/plugins/events.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/plugins/events.ts), where the `handlePluginEventsStream` function manages GET requests to `/admin/api/cms/plugins/events`. This handler creates a long-living `ReadableStream` that maintains an open HTTP connection to the client.

When a connection is established, the handler immediately emits an initial ping event to confirm readiness:

```typescript
// server/handlers/cms/plugins/events.ts
export function handlePluginEventsStream(req: Request): Response {
  if (req.method !== 'GET') return methodNotAllowed();

  const encoder = new TextEncoder();
  let closeStream: (() => void) | null = null;

  const stream = new ReadableStream<Uint8Array>({
    start(controller) {
      // …setup cleanup, heartbeat, lease…
      const unsubscribe = subscribePluginEvents(event =>
        controller.enqueue(encoder.encode(
          `event: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`
        ))
      );
      // initial ping
      controller.enqueue(encoder.encode(`event: ping\ndata: connected\n\n`));
    },
    cancel() { closeStream?.(); }
  });

  return new Response(stream, {
    status: 200,
    headers: {
      'content-type': 'text/event-stream',
      'cache-control': 'no-store',
      'x-accel-buffering': 'no',
    },
  });
}

```

### Plugin Event Broadcasting

The handler registers with the central **plugin event broadcaster** via `subscribePluginEvents`, located in [`src/core/plugins/eventBroadcaster.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/eventBroadcaster.ts). This subscription hub receives all plugin lifecycle events and forwards them to connected SSE streams. Each `PluginEvent` is serialized into the SSE wire format with the event kind as the event name and the JSON payload as the data.

### Connection Maintenance and Cleanup

To prevent reverse proxy timeouts (such as those in Vite or Nginx), the stream emits a heartbeat comment (`: heartbeat`) every 30 seconds. The connection also enforces a lease timeout of 2 minutes, automatically tearing down the stream when the request aborts or the lease expires to prevent orphaned connections.

## Client-Side EventSource Management

### Lazy Singleton Connection

The client implementation in [`src/admin/pages/plugins/utils/pluginEventStream.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/plugins/utils/pluginEventStream.ts) maintains a singleton `EventSource` instance that connects to `/admin/api/cms/plugins/events`. The connection follows a **lazy initialization** pattern: it opens only when the first consumer subscribes via `ensureConnected` and closes when the last listener unsubscribes using `disconnectIfIdle`.

```typescript
// src/admin/pages/plugins/utils/pluginEventStream.ts
let source: EventSource | null = null;
const listeners = new Set<(e: PluginEvent) => void>();

function ensureConnected() {
  if (source) return;
  source = new EventSource('/admin/api/cms/plugins/events', { withCredentials: true });
  for (const kind of PLUGIN_EVENT_KINDS) {
    source.addEventListener(kind, ev => {
      const data = safeParseValue(PluginEventSchema, JSON.parse((ev as MessageEvent).data));
      if (!data.ok) return console.warn('bad event payload', data.errors);
      listeners.forEach(l => l(data.value));
    });
  }
}

export function subscribePluginEvents(listener: (e: PluginEvent) => void): () => void {
  listeners.add(listener);
  ensureConnected();
  return () => {
    listeners.delete(listener);
    if (listeners.size === 0) source?.close();
  };
}

```

### Schema Validation and Event Distribution

For every event kind defined in `PLUGIN_EVENT_KINDS`, the EventSource registers a specific listener. Incoming messages undergo safe parsing using `safeParseValue(PluginEventSchema, …)` to validate against the TypeBox schema. Valid events are dispatched to all registered consumers, while malformed payloads trigger console warnings without breaking the stream.

## Event Schema and Supported Lifecycle Events

All plugin events conform to `PluginEventSchema`, defined in [`src/core/plugins/events/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/events/index.ts). The schema guarantees that every event includes at minimum a `kind` field (identifying the lifecycle stage), a `pluginId`, and a timestamp. The exported constant `PLUGIN_EVENT_KINDS` enumerates supported events including **installed**, **updated**, **crashed**, **restarted**, **enabled**, and **disabled**.

## Consuming Plugin Events

Admin UI components consume events by importing `subscribePluginEvents` and registering callbacks. The function returns a cleanup function that automatically manages the underlying EventSource lifecycle:

```typescript
import { subscribePluginEvents } from './pluginEventStream';

const stop = subscribePluginEvents(event => {
  if (event.kind === 'crash') {
    pushToast({ kind: 'error', title: 'Plugin Crashed', body: event.pluginId });
  }
});

// later, when the component unmounts
stop();

```

This pattern allows multiple independent components—including plugin lists, toast notifications, and navigation badges—to react to state changes while sharing a single HTTP connection.

## Summary

- **Server-sent events** provide a unidirectional stream from [`server/handlers/cms/plugins/events.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/plugins/events.ts) to admin clients at `/admin/api/cms/plugins/events`.
- The server emits **heartbeat comments** every 30 seconds and enforces a 2-minute lease to maintain connection health and prevent orphaned connections.
- **Lazy initialization** in [`src/admin/pages/plugins/utils/pluginEventStream.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/plugins/utils/pluginEventStream.ts) ensures the EventSource connects only when needed and closes automatically when idle.
- All events validate against **PluginEventSchema** and dispatch through a type-safe subscription API using `subscribePluginEvents`.
- The system supports comprehensive lifecycle notifications including installation, updates, crashes, and configuration changes.

## Frequently Asked Questions

### What is the SSE endpoint URL for plugin events in Instatic?

The endpoint is located at `/admin/api/cms/plugins/events` relative to the admin base path. This route is handled by `handlePluginEventsStream` in [`server/handlers/cms/plugins/events.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/plugins/events.ts) and returns a `text/event-stream` response with appropriate headers to disable buffering and caching.

### How does Instatic prevent SSE connection timeouts?

The implementation sends a heartbeat comment (`: heartbeat`) every 30 seconds to keep the connection alive through reverse proxies like Vite and Nginx. Additionally, the server enforces a 2-minute lease timeout that automatically closes the stream if the connection becomes stale, preventing resource exhaustion from orphaned connections.

### What happens when multiple admin tabs are open?

Each tab maintains its own independent EventSource connection to the server. The server-side `subscribePluginEvents` broadcaster in [`src/core/plugins/eventBroadcaster.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/eventBroadcaster.ts) distributes events to all connected streams, ensuring every open tab receives synchronized plugin lifecycle updates without requiring client-side coordination or broadcasting between tabs.

### How are malformed SSE messages handled on the client?

The client-side [`pluginEventStream.ts`](https://github.com/CoreBunch/Instatic/blob/main/pluginEventStream.ts) uses `safeParseValue(PluginEventSchema, …)` to validate incoming JSON payloads against the TypeBox schema. If parsing fails, the system logs a console warning and skips the malformed event, ensuring that schema violations in one message do not disrupt the event stream or crash the subscription handlers.