Auth
You want users to create accounts, log in, and access protected resources. Slingshot’s auth package gives you registration, login, JWT sessions, and route protection out of the box.
Add auth to your app
Section titled “Add auth to your app”bun add @lastshotlabs/slingshot-authimport { defineApp } from '@lastshotlabs/slingshot';import { createAuthPlugin } from '@lastshotlabs/slingshot-auth';
export default defineApp({ port: 3000, security: { signing: { secret: process.env.JWT_SECRET! }, }, plugins: [ createAuthPlugin({ auth: { roles: ['user', 'admin'], defaultRole: 'user' }, }), ], packages: [ // your packages here ],});That registers these routes automatically:
| Route | What it does |
|---|---|
POST /auth/register | Create a new account |
POST /auth/login | Log in, get a JWT token |
POST /auth/logout | End the session |
GET /auth/me | Get the current user |
Test it
Section titled “Test it”# Registercurl -X POST http://localhost:3000/auth/register \ -H "Content-Type: application/json" \ -d '{ "email": "alice@example.com", "password": "supersecret" }'
# Login — returns a JWT tokencurl -X POST http://localhost:3000/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "alice@example.com", "password": "supersecret" }'
# Use the token on protected routescurl http://localhost:3000/auth/me \ -H "Authorization: Bearer <token>"Protect your routes
Section titled “Protect your routes”Once auth is mounted, use auth: 'userAuth' on any route to require a valid session:
route.get({ path: '/dashboard', auth: 'userAuth', handler: ({ actor, respond }) => { return respond.json({ userId: actor.id, roles: actor.roles }); },});For entities, set routes.defaults to protect all generated routes at once:
const Task = defineEntity('Task', { namespace: 'app', fields: { id: field.string({ primary: true, default: 'uuid' }), title: field.string(), done: field.boolean({ default: false }), }, routes: { defaults: { auth: 'userAuth' }, },});Access the current user
Section titled “Access the current user”The auth middleware sets the actor on every request. Read it with getActor(c):
import { getActor, getActorId } from '@lastshotlabs/slingshot-core';
declare const c: Parameters<typeof getActor>[0];
// Full actor object — id, roles, tenantId, sessionId, claimsconst actor = getActor(c);
// Just the user ID (null for anonymous requests)const userId = getActorId(c);Add OAuth, MFA, and passkeys
Section titled “Add OAuth, MFA, and passkeys”These are opt-in. Enable them in the auth config when you need them:
createAuthPlugin({ auth: { roles: ['user', 'admin'], defaultRole: 'user', oauth: { google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }, }, mfa: { enabled: true }, passkeys: { enabled: true, rpName: 'My App', rpId: 'myapp.com' }, },});Role-based access
Section titled “Role-based access”Require a specific role with requireRole:
// @skip-typecheckimport { requireRole } from '@lastshotlabs/slingshot-auth';
router.use('/admin/*', requireRole('admin'));For entity routes, use route policy:
const Post = defineEntity('Post', { namespace: 'blog', fields: { id: field.string({ primary: true, default: 'uuid' }), title: field.string(), body: field.string(), }, routes: { defaults: { auth: 'userAuth' }, delete: { middleware: ['requireAdmin'] }, },});Lifecycle hooks
Section titled “Lifecycle hooks”Auth fires lifecycle hooks at well-defined moments:
| Hook | When it fires | Return value used for |
|---|---|---|
preLogin | Before credential verification | Side effects only |
postLogin | After session is persisted, before the JWT is signed | { customClaims } merged into the JWT |
preRegister | Before account creation | Side effects only |
postRegister | After account creation | Side effects only |
prePasswordChange | Before password is rotated | Side effects only |
postPasswordChange | After password is rotated | Side effects only |
preDeleteAccount | Before account row is removed | Side effects only |
postDeleteAccount | After account row is removed | Side effects only |
Each hook receives the relevant identifiers plus a HookContext that carries
request metadata (ip, userAgent, requestId) and a services?: HookServices
accessor for reading entities, resolving capabilities, and emitting events. See
HookServices for the full contract.
// @skip-typecheckimport { createAuthPlugin } from '@lastshotlabs/slingshot-auth';
interface ProfileAdapter { findByUserId(id: string): Promise<{ plan: 'free' | 'pro' } | null>;}
createAuthPlugin({ auth: { primaryField: 'email', roles: ['user'], defaultRole: 'user', hooks: { // postLogin can return { customClaims } — those keys are merged into the // signed JWT (after reserved JOSE claims are stripped). postLogin: async ({ userId, services }) => { if (!services) return undefined; const profiles = services.entities.get<ProfileAdapter>({ entity: 'Profile' }); const profile = await profiles.findByUserId(userId); if (!profile) return undefined; return { customClaims: { plan: profile.plan } }; },
// The other hooks are for side effects — emit events, write audit rows, etc. postRegister: async ({ userId, services }) => { services?.bus.emit('app:user.registered', { userId }); }, }, },});The postLogin hook is the only one whose return value is read; reserved JOSE
claims (exp, iat, nbf, iss, aud, jti, sub, sid) are always
stripped from customClaims before signing, so a hook cannot override the
identity claims auth itself sets.
Error semantics differ by phase:
pre*hooks are awaited inline. Throwing from one aborts the operation — the login, registration, password change, or delete fails and the error surfaces to the caller. Use this when you need to veto an operation.post*hooks are fire-and-forget. Errors are caught and logged viaconsole.error; they never abort the operation that triggered them. Use these for side effects (audit rows, downstream events, custom claims) where failure should not roll back the primary operation.
Bring your own auth
Section titled “Bring your own auth”You don’t need slingshot-auth. If you already have auth upstream (API gateway, reverse proxy,
your own JWT verification) or want to use a different auth library, write a plugin that:
- Sets the
actoron the Hono context - Registers a
RouteAuthRegistryso entity routes and framework features can enforceauth: 'userAuth'
// @skip-typecheckimport type { SlingshotPlugin } from '@lastshotlabs/slingshot-core';import type { Actor } from '@lastshotlabs/slingshot-core';
export function createMyAuthPlugin(): SlingshotPlugin { return { name: 'my-auth', setupMiddleware({ app }) { // Run on every request — resolve identity from your upstream app.use('*', async (c, next) => { const token = c.req.header('Authorization')?.replace('Bearer ', ''); if (token) { const payload = await verifyMyToken(token); // your verification const actor: Actor = { id: payload.sub, kind: 'user', tenantId: payload.tenantId ?? null, sessionId: null, roles: payload.roles ?? [], claims: payload, }; c.set('actor', actor); } await next(); }); }, setupPost({ registrar }) { // Register the auth registry so entity routes can use auth: 'userAuth' registrar.setRouteAuth({ userAuth: async (c, next) => { const actor = c.get('actor'); if (!actor?.id) return c.json({ error: 'Unauthorized' }, 401); await next(); }, requireRole(...roles) { return async (c, next) => { const actor = c.get('actor'); if (!actor?.roles?.some((r: string) => roles.includes(r))) { return c.json({ error: 'Forbidden' }, 403); } await next(); }; }, }); }, };}Then use it like slingshot-auth:
// @skip-typecheckimport { defineApp } from '@lastshotlabs/slingshot';import { appPackage } from './src/app/package';import { createMyAuthPlugin } from './src/auth/myAuthPlugin';
export default defineApp({ port: 3000, plugins: [createMyAuthPlugin()], packages: [appPackage],});Entity routes with auth: 'userAuth' will call your userAuth middleware. getActor(c),
getActorId(c), and all actor-based features (permissions, data scoping, audit) work
automatically because they read from c.get('actor').
Next in your journey
Section titled “Next in your journey”Auth tells you who the actor is. The next step is what they’re allowed to do:
- Permissions — resource-level access control built on top of the actor model.
Go deeper into auth
Section titled “Go deeper into auth”- Auth Setup Example — end-to-end auth walkthrough with OAuth, MFA, and passkeys
- SaaS Foundations — auth + organizations + permissions for multi-tenant
- Maturity and Package Status — current package stability expectations