Auth Setup
Experimental:
@lastshotlabs/slingshot-authis currently pre-1.0 (0.0.2) in this worktree. Read Auth and Maturity and Package Status before you commit to this package in a product.
Add slingshot-auth and you get registration, login, session management, password reset, OAuth, MFA, and passkeys. All from one plugin, zero boilerplate.
Source-backed example app: examples/with-auth/
Install
Section titled “Install”bun add @lastshotlabs/slingshot @lastshotlabs/slingshot-auth hono zodMinimum setup
Section titled “Minimum setup”import { 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' }, db: { auth: 'memory', sessions: 'memory', oauthState: 'memory' }, }), ],});Mounted routes
Section titled “Mounted routes”| Method | Path | Description |
|---|---|---|
POST | /auth/register | Create an account |
POST | /auth/login | Log in, receive a JWT |
POST | /auth/logout | End the session |
GET | /auth/me | Current user (requires JWT) |
PATCH | /auth/me | Update profile fields |
POST | /auth/refresh | Rotate the session token |
POST | /auth/password-reset/request | Send a password reset email |
POST | /auth/password-reset/confirm | Complete the reset |
Enabling OAuth, MFA, or passkeys adds additional routes (see below).
Protecting routes
Section titled “Protecting routes”Import userAuth and requireRole from slingshot-auth and apply them as Hono middleware before your handler.
import { createRoute, createRouter, getActor } from '@lastshotlabs/slingshot';import { requireRole, userAuth } from '@lastshotlabs/slingshot-auth';
declare function getUserCount(): Promise<number>;
export const router = createRouter();
// Require a valid JWT — returns 401 automatically if missing or invalidrouter.use('/account/*', userAuth);
router.openapi( createRoute({ method: 'get', path: '/account/profile', responses: { 200: { description: 'Current user', content: { 'application/json': { schema: { type: 'object', properties: { userId: { type: 'string' }, roles: { type: 'array', items: { type: 'string' }, }, }, required: ['userId', 'roles'], }, }, }, }, }, }), // @ts-expect-error -- simplified doc example, handler type is narrower than openapi expects c => c.json({ userId: getActor(c).id!, roles: getActor(c).roles ?? [] }),);
// Require a specific role — returns 403 if the user doesn't have itrouter.use('/account/admin/*', requireRole('admin'));
router.openapi( createRoute({ method: 'get', path: '/account/admin/stats', responses: { 200: { description: 'Admin stats', content: { 'application/json': { schema: { type: 'object', properties: { userCount: { type: 'number' }, }, required: ['userCount'], }, }, }, }, }, }), async c => { const count = await getUserCount(); return c.json({ userCount: count }) as never; },);userAuth verifies the JWT from the Authorization: Bearer header or the x-user-token header and sets the actor on the context. requireRole checks the actor’s roles value — always mount it after userAuth.
Using the token
Section titled “Using the token”Register, log in, then pass the token as a Bearer header:
# Registercurl -X POST http://localhost:3000/auth/register \ -H "Content-Type: application/json" \ -d '{ "email": "you@example.com", "password": "hunter2" }'
# Log incurl -X POST http://localhost:3000/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "you@example.com", "password": "hunter2" }'# → { "token": "eyJ..." }
# Call a protected routecurl http://localhost:3000/account/profile \ -H "Authorization: Bearer eyJ..."Persistent storage
Section titled “Persistent storage”Each adapter is independent. Mix and match:
export default defineApp({ db: { sqlite: `${import.meta.dir}/data/app.db` }, plugins: [ createAuthPlugin({ auth: { roles: ['user', 'admin'], defaultRole: 'user' }, db: { auth: 'sqlite', sessions: 'sqlite', oauthState: 'sqlite' }, }), ],});SQLite is built into Bun. On Node.js, install better-sqlite3 first.
bun add @lastshotlabs/slingshot-postgresexport default defineApp({ db: { postgres: process.env.DATABASE_URL! }, plugins: [ createAuthPlugin({ auth: { roles: ['user', 'admin'], defaultRole: 'user' }, db: { auth: 'postgres', sessions: 'postgres', oauthState: 'postgres' }, }), ],});bun add ioredisexport default defineApp({ db: { sqlite: `${import.meta.dir}/data/app.db`, redis: process.env.REDIS_URL!, }, plugins: [ createAuthPlugin({ db: { auth: 'sqlite', sessions: 'redis', oauthState: 'memory' }, }), ],});A common production setup: durable user records in SQLite or Postgres, high-throughput sessions in Redis.
bun add @lastshotlabs/slingshot-oauth arcticcreateAuthPlugin({ auth: { roles: ['user'], defaultRole: 'user', oauth: { providers: { google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, redirectUri: 'http://localhost:3000/auth/oauth/google/callback', }, }, postRedirect: '/dashboard', }, }, db: { auth: 'memory', sessions: 'memory', oauthState: 'memory' },});OAuth routes mount automatically: GET /auth/oauth/:provider → GET /auth/oauth/:provider/callback. Redirect the user to /auth/oauth/google to start the flow. After the callback, they have a session.
MFA (TOTP)
Section titled “MFA (TOTP)”bun add otpauthauth: { roles: ['user'], defaultRole: 'user', mfa: { totp: true } }Adds POST /auth/mfa/setup, POST /auth/mfa/verify, and DELETE /auth/mfa.
Passkeys (WebAuthn)
Section titled “Passkeys (WebAuthn)”bun add @simplewebauthn/serverauth: { roles: ['user'], defaultRole: 'user', passkeys: { rpName: 'My App', rpId: 'localhost', origin: 'http://localhost:3000', },}Adds begin/complete routes for both registration and authentication.
When emailVerification.required is enabled, passkey authentication follows the same gate as password login: unverified email accounts cannot sign in with WebAuthn until the address is verified.
Email verification
Section titled “Email verification”Require users to verify their email before they can log in:
auth: { roles: ['user'], defaultRole: 'user', emailVerification: { required: true, tokenExpirySeconds: 86400, // 24 hours },}Password policy
Section titled “Password policy”auth: { passwordPolicy: { minLength: 12, requireUppercase: true, requireNumbers: true, },}Multiple roles
Section titled “Multiple roles”Declare all roles upfront. Auth stores them in the JWT. Access them via getActor(c).roles:
auth: { roles: ['user', 'moderator', 'admin'], defaultRole: 'user' }Assign roles through the admin plugin or directly through the auth adapter in your own routes.
Where to go next
Section titled “Where to go next”- slingshot-auth — full config reference with all options
- Forum App — add
slingshot-communityon top of auth - SaaS Foundations — add organizations and per-resource permissions
- OpenAPI — how
userAuthandrequireRoleappear in the spec