Event Bus
If you are building an app, start with Core Features: Events and the Event Bus.
This page is the lower-level plugin and authoring reference view of the same system.
The event bus is how plugins talk to each other without importing each other. Auth emits auth:user.created. Mail subscribes and sends the welcome email. No auth-to-mail dependency, no circular imports, no tight coupling.
That is the point. When auth deletes a user, it should not need to know that mail exists, or notes exist, or anything else. It fires the event. Every subscriber gets it and does its own thing.
Accessing the bus
Section titled “Accessing the bus”import type { SlingshotPlugin } from '@lastshotlabs/slingshot-core';import { getContext } from '@lastshotlabs/slingshot-core';
const plugin: SlingshotPlugin = { name: 'notes', setupPost({ app, bus }) { const ctx = getContext(app); void ctx; bus.on('auth:user.created', () => {}); },};
void plugin;Emitting events
Section titled “Emitting events”import { InProcessAdapter } from '@lastshotlabs/slingshot-core';
const bus = new InProcessAdapter();
bus.emit('auth:user.created', { userId: 'user-123', email: 'user@example.com', tenantId: null,});
bus.emit('app:ready', { plugins: ['slingshot-auth', 'notes'] });Subscribing to events
Section titled “Subscribing to events”Always subscribe in setupPost - that is after all plugins have finished setupRoutes, which means all publishers are wired before any subscriber tries to listen.
import type { SlingshotPlugin } from '@lastshotlabs/slingshot-core';
async function sendWelcomeEmail(email: string): Promise<void> { void email;}
async function deleteAllUserData(userId: string): Promise<void> { void userId;}
const plugin: SlingshotPlugin = { name: 'notes', setupPost({ bus }) { bus.on('auth:user.created', async ({ email }) => { if (email) { await sendWelcomeEmail(email); } });
bus.on('auth:user.deleted', async ({ userId }) => { await deleteAllUserData(userId); }); },};
void plugin;Extending the event map
Section titled “Extending the event map”Add your package’s events to the shared type map via module augmentation:
interface Note { id: string; title: string;}
declare module '@lastshotlabs/slingshot-core' { interface SlingshotEventMap { 'note.created': { userId: string; noteId: string; title: string }; 'note.updated': { userId: string; noteId: string; changes: Partial<Note> }; 'note.deleted': { userId: string; noteId: string }; }}After this, bus.emit('note.created', ...) is fully typed and bus.on('note.created', payload => ...) gets the correct payload type.
Client-safe events and SSE
Section titled “Client-safe events and SSE”The SSE endpoint only forwards events you have explicitly flagged as client-safe. This is not opt-out - it is opt-in. Internal events such as auth tokens, security signals, and delivery internals never reach browser clients regardless of what you register.
Register externally deliverable events before publishing them:
import { type SlingshotPlugin, defineEvent } from '@lastshotlabs/slingshot-core';
declare module '@lastshotlabs/slingshot-core' { interface SlingshotEventMap { 'note.created': { userId: string; noteId: string }; 'note.updated': { userId: string; noteId: string }; 'note.deleted': { userId: string; noteId: string }; }}
const plugin: SlingshotPlugin = { name: 'notes', setupMiddleware({ events }) { events.register( defineEvent('note.created', { ownerPlugin: 'notes', exposure: ['client-safe'], resolveScope(payload) { return { userId: payload.userId, actorId: payload.userId }; }, }), ); events.register( defineEvent('note.updated', { ownerPlugin: 'notes', exposure: ['client-safe'], resolveScope(payload) { return { userId: payload.userId, actorId: payload.userId }; }, }), ); events.register( defineEvent('note.deleted', { ownerPlugin: 'notes', exposure: ['client-safe'], resolveScope(payload) { return { userId: payload.userId, actorId: payload.userId }; }, }), ); },};
void plugin;Forbidden namespaces are never forwarded regardless of event-definition exposure:
| Namespace | Reason |
|---|---|
security.* | Internal security events |
auth:* | Auth lifecycle with PII or tokens |
community:delivery.* | Internal delivery signals |
push:* | Push delivery internals |
app:* | Framework lifecycle |
Draining events in tests
Section titled “Draining events in tests”Event handlers run asynchronously - emit fires and returns immediately. In tests, that means your assertions can run before the handlers finish. bus.drain() waits for all pending handlers to settle before you assert.
import { expect, test } from 'bun:test';import { InProcessAdapter } from '@lastshotlabs/slingshot-core';
test('drains async listeners before asserting', async () => { const bus = new InProcessAdapter(); const sentEmails: string[] = [];
bus.on('auth:user.created', async ({ email }) => { if (email) { sentEmails.push(email); } });
bus.emit('auth:user.created', { userId: 'user-1', email: 'test@example.com', tenantId: null, });
await bus.drain();
expect(sentEmails).toEqual(['test@example.com']);});Built-in events
Section titled “Built-in events”These come from the core event map - no augmentation needed:
| Event | Payload |
|---|---|
auth:user.created | { userId, email?, tenantId? } |
auth:user.deleted | { userId, tenantId? } |
auth:login | { userId, sessionId, tenantId? } |
auth:logout | { userId, sessionId } |
auth:mfa.enabled | { userId, method } |
auth:mfa.disabled | { userId, method? } |
security.rate_limit.exceeded | { key?, ip?, meta? } |
app:ready | { plugins } |
app:shutdown | { signal } |
Multi-instance delivery
Section titled “Multi-instance delivery”The default InProcessAdapter delivers events only within the current process. For multi-instance deployments, switch to a shared event bus so events cross instances:
import { createBullMQAdapter } from '@lastshotlabs/slingshot-bullmq';
export default defineApp({ eventBus: createBullMQAdapter({ connection: { host: process.env.REDIS_HOST!, port: 6379 }, }),});BullMQ is the built-in option. Other brokers still work through the registry-backed custom event-bus path. See Custom Event Bus for the full configuration.
See also
Section titled “See also”- Internals: Event Bus -
InProcessAdapter, transport contract, andSlingshotEventMapextension - Custom Event Bus - BullMQ plus registry-backed custom buses
- Realtime with SSE - forwarding client-safe events to browsers