Context and Request Model
Slingshot separates app-scoped state (things that exist for the lifetime of the process) from request-scoped state (things that exist for one HTTP request). Understanding the split makes everything from testing to middleware to realtime click.
App-scoped state: SlingshotContext
Section titled “App-scoped state: SlingshotContext”SlingshotContext is the runtime state for one assembled app instance. Access it with
getContext(app):
// @skip-typecheckimport { createApp, getContext } from '@lastshotlabs/slingshot';
const { app } = await createApp({ packages: [blogPackage] });const ctx = getContext(app);What lives on the context
Section titled “What lives on the context”| Property | What it is |
|---|---|
ctx.config | The frozen, resolved app config |
ctx.events | The event bus — publish, on, off |
ctx.persistence | Database and storage handles |
ctx.plugins | Registered plugin runtime state |
ctx.ws | WebSocket room state and broadcast helpers |
ctx.clear() | Resets in-memory state (for tests) |
ctx.destroy() | Tears down connections and runtime (cleanup) |
Use it in plugins
Section titled “Use it in plugins”Plugins get the context in their lifecycle hooks:
// @skip-typecheckconst analyticsPlugin: SlingshotPlugin = { name: 'analytics', setupPost({ ctx }) { // Access the event bus from app context ctx.events.on('auth:user.created', async ({ userId }) => { await trackSignup(userId); }); }, teardown({ ctx }) { // Clean up on shutdown console.log('Analytics shutting down'); },};Use it in tests
Section titled “Use it in tests”// @skip-typecheckimport { createApp, getContext } from '@lastshotlabs/slingshot';
let app: Awaited<ReturnType<typeof createApp>>['app'];let ctx: ReturnType<typeof getContext>;
beforeEach(async () => { const result = await createApp({ packages: [blogPackage] }); app = result.app; ctx = getContext(app);});
afterEach(async () => { await ctx.destroy();});
test('creates a post', async () => { const res = await app.request('/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: 'Test Post' }), }); expect(res.status).toBe(201);});
test('state is isolated between tests', async () => { // ctx.clear() is called implicitly — each test starts fresh const res = await app.request('/posts'); const data = await res.json(); expect(data.items).toHaveLength(0);});Request-scoped state: the actor
Section titled “Request-scoped state: the actor”Every HTTP request has an identity — who is making it. In Slingshot, that is the actor.
In package route handlers
Section titled “In package route handlers”Package route handlers receive the actor directly:
// @skip-typecheckroute.get({ path: '/dashboard', auth: 'userAuth', handler: ({ actor, respond }) => { return respond.json({ userId: actor.id, roles: actor.roles, tenantId: actor.tenantId, }); },});In middleware or raw Hono routes
Section titled “In middleware or raw Hono routes”Use getActor(c) and related helpers:
// @skip-typecheckimport { getActor, getActorId, getRequestTenantId } from '@lastshotlabs/slingshot-core';
// Full actor objectconst actor = getActor(c);// actor.id — user ID (or null for anonymous)// actor.kind — 'user', 'service', 'anonymous'// actor.roles — role strings// actor.tenantId — the actor's home tenant// actor.sessionId — current session// actor.claims — JWT claims
// Just the user IDconst userId = getActorId(c);
// The request-scoped tenant (from URL, header, or resolver)const tenantId = getRequestTenantId(c);The Actor shape
Section titled “The Actor shape”interface Actor { id: string | null; kind: 'user' | 'service' | 'anonymous'; roles?: string[]; tenantId?: string; sessionId?: string; claims?: Record<string, unknown>;}What belongs where
Section titled “What belongs where”| You need… | Read from |
|---|---|
| Who is making this request | actor or getActor(c) |
| Just the user ID | getActorId(c) |
| The current tenant | getRequestTenantId(c) |
| Route params, query string, request body | params, query, body |
| Event bus | ctx.events (app-scoped) |
| Database handles | ctx.persistence |
| WebSocket rooms | ctx.ws |
| App config | ctx.config (frozen) |
Common mistakes
Section titled “Common mistakes”-
Do not stash
ctxin a module-global variable. UsegetContext(app)for the specific app instance. This matters when running multiple app instances in one process (common in tests). -
Do not treat request identity as app state. The actor belongs to the request, not the app. If you need to correlate across requests, use event payloads or database state.
-
Do not mutate
ctx.config. It is frozen after assembly. If you need dynamic config, use a secret provider or environment variables at boot time.
- createServer and createApp — the assembly step
- Middleware — where request-scoped state intersects middleware chains
- SlingshotContext reference — full context API for plugin authors