CoreRegistrar
CoreRegistrar is the startup-time handoff object. Plugins call setter methods during setup to publish shared capabilities into the app. Other plugins consume them without direct cross-package imports.
slingshot-auth calls config.registrar.setRouteAuth(...) during setupPost so other plugins and route files can read ctx.routeAuth without importing auth internals directly.
What the registrar holds
Section titled “What the registrar holds”| Capability | Registrar method | Published by | Read from context via |
|---|---|---|---|
Route auth helpers (userAuth, requireRole) | setRouteAuth() | slingshot-auth | getRouteAuth(ctx) or ctx.routeAuth |
| Request actor resolver | setRequestActorResolver() | slingshot-auth | getRequestActorResolver(ctx) or ctx.actorResolver |
| Email templates | addEmailTemplates() | slingshot-auth, custom plugins | ctx.emailTemplates |
| Rate limit adapter | setRateLimitAdapter() | slingshot-auth | ctx.rateLimitAdapter |
| Cache adapters | addCacheAdapter() | Any plugin | ctx.cacheAdapters |
| Fingerprint builder | setFingerprintBuilder() | slingshot-auth | ctx.fingerprintBuilder |
Accessing capabilities from the registrar
Section titled “Accessing capabilities from the registrar”After the plugin lifecycle completes, the registrar drains its values into SlingshotContext. Read published capabilities through the standalone accessor helpers:
import type { SlingshotPlugin } from '@lastshotlabs/slingshot-core';import { getContext, getRequestActorResolver, getRouteAuth } from '@lastshotlabs/slingshot-core';
const plugin: SlingshotPlugin = { name: 'example', setupPost({ app }) { const ctx = getContext(app);
// Read what other plugins have published const userAuth = getRouteAuth(ctx)?.userAuth; const actorResolver = getRequestActorResolver(ctx); void userAuth; void actorResolver; },};
void plugin;Publishing a request actor resolver
Section titled “Publishing a request actor resolver”If your plugin provides user lookup (for WebSocket auth, admin, etc.), publish it during setupPost via config.registrar:
import { ANONYMOUS_ACTOR, type Actor, type SlingshotPlugin } from '@lastshotlabs/slingshot-core';
const adapter = { async verifyToken(token: string): Promise<{ id: string } | null> { return token === 'valid-token' ? { id: 'user-1' } : null; },};
const plugin: SlingshotPlugin = { name: 'example', setupPost({ config }) { config.registrar.setRequestActorResolver({ async resolveActor(req: Request): Promise<Actor> { const token = req.headers.get('authorization')?.replace('Bearer ', ''); if (!token) return ANONYMOUS_ACTOR; const user = await adapter.verifyToken(token); if (!user) return ANONYMOUS_ACTOR; return { ...ANONYMOUS_ACTOR, id: user.id, kind: 'user' }; }, }); },};
void plugin;Publishing email templates
Section titled “Publishing email templates”Register templates during setupPost so slingshot-mail picks them up after all plugins have run:
import type { SlingshotPlugin } from '@lastshotlabs/slingshot-core';
const digestHtmlTemplate = '<p>Your weekly digest</p>';const digestTextTemplate = 'Your weekly digest';
const plugin: SlingshotPlugin = { name: 'example', setupPost({ config }) { config.registrar.addEmailTemplates({ 'notes.weekly-digest': { subject: 'Your weekly digest', html: digestHtmlTemplate, text: digestTextTemplate, }, }); },};
void plugin;Registrar vs pluginState
Section titled “Registrar vs pluginState”They solve different problems.
The registrar holds shared cross-package capabilities with a stable contract — things the framework itself consumes, or things that route files import directly. pluginState holds arbitrary plugin runtime that sibling plugins discover dynamically at setupPost time.
Use the registrar for:
- Route middleware that route files import directly
- Capabilities the framework consumes (request actor resolver, cache adapters)
- Named template registries
Use pluginState for:
- Plugin-specific adapters and runtimes
- Optional integrations other plugins discover in
setupPost
Why the registrar exists
Section titled “Why the registrar exists”Without it, there’s no clean way to share runtime capabilities across packages. The alternatives are all worse.
Module-level singletons break test isolation and make multiple app instances impossible — two createApp() calls stomp on the same state.
Direct cross-package imports create tight coupling. slingshot-notes importing directly from slingshot-auth internals means you can’t swap auth implementations without touching every dependent package.
Hardcoding in the framework doesn’t scale. The framework can’t know about your custom plugins, and it shouldn’t need to.
The registrar gives startup a structured handoff point. Auth calls config.registrar.setRouteAuth(...) during its setupPost. After all plugins run, createApp() drains those values into SlingshotContext — your route files read them via getRouteAuth(ctx) without importing auth internals. Each app instance gets its own registrar, so tests that run two apps in the same process don’t interfere with each other.
See also
Section titled “See also”- SlingshotContext — where the registrar snapshot lives at runtime
- Plugin Interface — lifecycle phases where the registrar is populated
- Internals: Context and Registrar — implementation details