BackendAuthentication
Custom Provider
Implement a custom authentication provider using the AuthProvider interface.
Build your own auth provider when you need to integrate with an existing auth system, use a custom JWT library, or handle non-standard token formats.
import { createEngine } from '@superapp/backend'
import type { AuthProvider } from '@superapp/backend/auth'
const customAuth: AuthProvider = {
async verifyToken(token) {
const payload = await myJwtLib.verify(token, process.env.JWT_PUBLIC_KEY!)
return payload
},
async findUser(payload, db) {
const user = await db
.selectFrom('main.users')
.selectAll()
.where('id', '=', payload.sub)
.executeTakeFirst()
return user ?? null
},
async resolveSession(user, db) {
return { ...user, role: user.role }
},
}
const engine = createEngine({
connections: {
main: { type: 'postgres', url: process.env.PG_URL! },
},
auth: customAuth,
})AuthProvider Interface
interface AuthProvider {
/** Verify the JWT token and return the decoded payload */
verifyToken(token: string): Promise<JWTPayload>
/** Look up the user record from the decoded JWT payload */
findUser(payload: JWTPayload, db: QueryBuilder): Promise<User | null>
/** Optional: enrich the user object with session data */
resolveSession?(user: User, db: QueryBuilder): Promise<EnrichedUser>
/** Optional: expose auth-related route handlers */
routes?: Record<string, RouteHandler>
}
interface JWTPayload {
sub?: string
[key: string]: unknown
}Implementation Requirements
verifyToken
Must throw an error if the token is invalid or expired. The engine catches this and returns 401 Unauthorized.
async verifyToken(token) {
try {
return await jose.jwtVerify(token, publicKey, {
issuer: 'https://auth.myapp.com',
algorithms: ['RS256'],
})
} catch {
throw new Error('Invalid token')
}
}findUser
Return null if the user is not found. The engine returns 401 Unauthorized in that case.
async findUser(payload, db) {
const user = await db
.selectFrom('main.users')
.select(['id', 'email', 'name', 'role'])
.where('id', '=', payload.sub)
.where('is_active', '=', true)
.executeTakeFirst()
return user ?? null
}resolveSession (optional)
Add any properties that your permissions need to reference via $user.*:
async resolveSession(user, db) {
const orgs = await db
.selectFrom('main.members')
.select(['organization_id', 'role'])
.where('user_id', '=', user.id)
.execute()
return {
...user,
org_ids: orgs.map(o => o.organization_id),
current_org_id: orgs[0]?.organization_id ?? null,
}
}routes (optional)
Expose custom auth endpoints under /auth/*:
routes: {
'POST /auth/login': async (req) => {
const { email, password } = await req.json()
const token = await myAuth.login(email, password)
return Response.json({ token })
},
'POST /auth/logout': async (req) => {
await myAuth.logout(req.headers.get('authorization'))
return Response.json({ ok: true })
},
}Example: Firebase Auth
import { createEngine } from '@superapp/backend'
import type { AuthProvider } from '@superapp/backend/auth'
import admin from 'firebase-admin'
admin.initializeApp({ credential: admin.credential.applicationDefault() })
const firebaseAuth: AuthProvider = {
async verifyToken(token) {
const decoded = await admin.auth().verifyIdToken(token)
return { sub: decoded.uid, email: decoded.email }
},
async findUser(payload, db) {
const user = await db
.selectFrom('main.users')
.selectAll()
.where('firebase_uid', '=', payload.sub)
.executeTakeFirst()
return user ?? null
},
async resolveSession(user, db) {
return { ...user }
},
}
const engine = createEngine({
connections: {
main: { type: 'postgres', url: process.env.PG_URL! },
},
auth: firebaseAuth,
})