@robojs/auth
Modern authentication for Robo.js projects powered by Auth.js. This plugin drops the familiar /api/auth/* surface into your bot or activity, wires up OAuth and email/password flows, and exposes helper APIs for both server runtimes and client-facing experiences.
β π Documentation: Getting started
β π Community: Join our Discord server
Installationβ
Install and register the plugin:
npx robo add @robojs/auth// config/plugins/robojs/auth.ts
import { discord, google } from '@robojs/auth/providers'
import { EmailPassword, createFlashcoreAdapter } from '@robojs/auth'
import type { AuthPluginOptions } from '@robojs/auth'
export default <AuthPluginOptions>{
secret: process.env.AUTH_SECRET,
appName: 'Acme Dashboard',
adapter: createFlashcoreAdapter({ secret: process.env.AUTH_SECRET! }),
providers: [
google({ clientId: process.env.GOOGLE_ID!, clientSecret: process.env.GOOGLE_SECRET! }),
discord({ clientId: process.env.DISCORD_ID!, clientSecret: process.env.DISCORD_SECRET! }),
EmailPassword()
]
}
Running npx robo add @robojs/auth now offers to add an AUTH_SECRET entry to every .env/.env.* mode in your project. Accepting the prompt generates a fresh 64-character secret so you start with secure defaults instead of the sample token from the docs.
Boot Robo.js and point your UI at /api/auth (customise via basePath). Robo will index the plugin, scaffold the REST routes, enable the built-in email/password flow, and keep Auth.js callbacks/event hooks in sync with your configuration.
Prefer Prisma over Flashcore? Swap the adapter and keep the rest of your config untouched by exporting from the same config/plugins/robojs/auth.ts entry point:
// config/plugins/robojs/auth.ts
import { PrismaClient } from '@prisma/client'
import { EmailPassword, createPrismaAdapter } from '@robojs/auth'
import type { AuthPluginOptions } from '@robojs/auth'
const prisma = new PrismaClient()
export default <AuthPluginOptions>{
secret: process.env.AUTH_SECRET,
appName: 'Acme Dashboard',
adapter: createPrismaAdapter({
client: prisma,
secret: process.env.AUTH_SECRET!
}),
providers: [EmailPassword()]
}
Heads up: Install
@auth/prisma-adapter(and@prisma/client) in your project before wiring this adapter.
The Prisma adapter stays compatible with Auth.js' recommended schema while layering in password helpers, reset tokens, and pagination utilities. See the cheatsheet below for the schema blocks to add alongside your existing Auth.js models.
What You Getβ
- Drop-in Auth.js REST endpoints mirroring
/api/auth/*(providers, sign-in/out, callback, session, csrf). - Storage adapters with zero configuration Flashcore defaults and an optional Prisma variant that layers password helpers onto Auth.js' schema.
- Email + password support on by default with hashed storage, password reset tokens, and opt-in authorisation hooks.
- Templated email flows that plug into Auth.js providers (Resend, Postmark, SendGrid, Nodemailer, β¦) or your own deliver function, including fully rendered HTML/text templates.
- Typed client helpers for UI surfaces to sign in, sign out, fetch providers, sessions, and CSRF tokens.
- Server utilities for grabbing sessions/tokens, configuring runtime state, and bridging Robo requests into Auth.js handlers.
Configuration Overviewβ
AuthPluginOptions extends the Auth.js config you already know:
| Option | Default | Notes |
|---|---|---|
basePath | /api/auth | Prefix for generated routes. |
appName | 'Robo.js' | Display name injected into default emails and available as ctx.appName. |
secret | required in prod | Used for JWT + token hashing. Reads AUTH_SECRET/NEXTAUTH_SECRET. |
url | env (AUTH_URL/NEXTAUTH_URL) or http://localhost:3000 | Canonical URL for Auth.js callbacks. |
redirectProxyUrl | AUTH_REDIRECT_PROXY_URL | Useful for preview deployments. |
providers | [] | Array of Auth.js providers. Helpers live under @robojs/auth/providers. |
adapter | Flashcore | Swap for any Auth.js adapter when you outgrow the default storage. |
session.strategy | jwt (or database when adapter present) | Supports maxAge & updateAge. |
cookies, callbacks, events, pages | Auth.js defaults | Use the same shapes as upstream Auth.js. |
email / emails | {} | Override templates, configure mailers, or bind to third-party transports. |
upstream | unset | Forward all Auth.js routes to another Robo instance with { baseUrl, basePath?, headers?, cookieName?, secret?, sessionStrategy?, fetch? }. |
Need validation? Use the exported authPluginOptionsSchema or call normalizeAuthOptions(options) to apply defaults before passing into other tooling.
Built-in Email + Password Storageβ
When no custom adapter is supplied, Robo stores hashed passwords, reset tokens, and user metadata in Flashcore. The bundled email/password provider (documented later) is turned on by default so your UI can immediately present email + password fields without extra wiring. Swap the adapter whenever you introduce your own persistence layer.
Signup Redirectsβ
By default, successful signups redirect to /. You can customize this by:
- Sending a
callbackUrlin the signup request body. - Setting
pages.newUserin your plugin configuration:
export default <AuthPluginOptions>{
// ...
pages: {
newUser: '/dashboard' // Redirect to dashboard after signup
}
}
Proxying Another Robo Projectβ
Need the same Auth.js instance across multiple Robo apps? Set upstream.baseUrl to the canonical deployment and the plugin will proxy every /api/auth/* route (plus getServerSession/getToken) to that remote service.
// config/plugins/robojs/auth.ts
import type { AuthPluginOptions } from '@robojs/auth'
export default <AuthPluginOptions>{
basePath: '/api/auth',
upstream: {
baseUrl: process.env.AUTH_UPSTREAM_URL!,
headers: { 'x-api-key': process.env.AUTH_PROXY_KEY! }
}
}
Provide upstream.secret if you want getToken() to decode JWT payloads locally; otherwise call it with { raw: true } or use getServerSession() which always consults the upstream service.
Flashcore Adapter Cheatsheetβ
Out of the box the plugin persists everything to Flashcore namespaces. Swap to your own adapter by exporting it from AuthPluginOptions. When you enable (or customise) the credentials flow, make sure your adapter satisfies the extended PasswordAdapter contractβuse assertPasswordAdapter(adapter) in development to catch missing methods early.
Listing Usersβ
import { listUsers, listUserIds } from '@robojs/auth'
const page = await listUsers() // { users, page, pageCount, total }
const ids = await listUserIds(2) // { ids, page, pageCount, total }
Prisma Adapter Cheatsheetβ
Already using Auth.js with Prisma? createPrismaAdapter layers Robo's password helpers and reset-token flow on top of the official @auth/prisma-adapter so you can reuse your existing tables (and migrations) with almost no changes.
Schema Additionsβ
Add a single model alongside the standard Auth.js schema. It extends the user relation with argon2id hashes while keeping pagination fast:
model Password {
id String @id @default(cuid())
userId String @unique
email String @unique
hash String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([email])
}
PostgreSQL users can swap String for @db.Citext on email to keep lookups case-insensitive; other drivers can rely on the adapter's lower-cased mirror. Password reset links continue to use Auth.js' built-in verification token table, so no additional schema is required. Run npx prisma migrate dev --name add-auth-passwords (or your preferred command) after updating the schema.
Adapter Optionsβ
createPrismaAdapter accepts the following options (after installing @auth/prisma-adapter):
clientβ yourPrismaClientinstance.secretβ standard Auth.js secret for session/token helpers (typicallyAUTH_SECRET).hashParametersβ optional argon2id tuning; pass overrides to rehash on verify when params change.models.passwordβ override the password model name if you already migrated with different identifiers.
The adapter returns the same extended PasswordAdapter contract as the Flashcore version, so the built-in EmailPassword provider works without changes.
Listing Usersβ
import { listPrismaUsers, listPrismaUserIds } from '@robojs/auth'
const { users } = await listPrismaUsers(prisma)
const { ids } = await listPrismaUserIds(prisma, { page: 1, pageSize: 100 })
Both helpers accept optional { page, pageSize, orderBy, where } parameters and share the same return shape as the Flashcore utilities, making it easy to drop into dashboards or admin tooling.
Client APIβ
All client helpers are exported via @robojs/auth/client (and re-exported from the package root). Each function accepts optional overrides for basePath, baseUrl, headers, or a custom fetch implementationβideal for activities or external UIs.
| Function | Description |
|---|---|
signIn(providerId, body?, options?) | POST to /signin (or a provider-specific route) to begin the Auth.js flow. Pass extra form fields via body. |
signOut(options?) | POST to /signout and clear the active session cookie. |
getSession(options?) | GET /session and return the current Session object (or null). |
getProviders(options?) | GET /providers for a runtime list of configured Auth.js providers. |
getCsrfToken(options?) | GET /csrf to retrieve the token required for form POSTs. |
import { signIn, getSession, getProviders } from '@robojs/auth/client'
await signIn('google')
const session = await getSession({ headers: { cookie: request.headers.get('cookie') ?? '' } })
const providers = await getProviders()
Set baseUrl when calling these helpers to speak to a different originβideal when a frontend Robo app proxies to a backend deployment that owns the Auth.js adapter.
Redirect Mode (Proxy)β
OAuth flows often require topβlevel navigation. When using a crossβorigin proxy, fetch to /signin/:provider produces an opaqueredirect and does not change the location. The client helpers support an explicit redirect mode so you can opt into a safe, CSRFβprimed navigation without bespoke code.
New signatures (backwards compatible):
-
signIn(provider, options?, proxy?, redirect?)options:{ csrfToken?: string; callbackUrl?: string; [extra] }(also carries any providerβspecific fields for credentials flows)proxy:{ baseUrl: string; basePath?: string }(you may also passheadersor a customfetch)redirect:boolean | 'manual'
-
signOut(options?, proxy?, redirect?)options:{ csrfToken?: string; callbackUrl?: string }proxy:{ baseUrl: string; basePath?: string }redirect:boolean | 'manual'
Return values when redirect is provided:
redirect: trueβ{ ok: true, redirected: true }and performswindow.location.assign(url).redirect: 'manual'β{ ok: true, url }so frameworks can issue their own redirect.redirect: falseβ{ ok: boolean, url?: string, error?: string }using the fetchβbased flow.
If redirect is omitted, behavior is unchanged and the legacy helpers return the raw Response for backwards compatibility.
Examples
import { signIn, signOut } from '@robojs/auth/client'
// Top-level client redirect
await signIn('discord', { callbackUrl: window.location.origin + '/dashboard' }, { baseUrl: 'https://auth.example.com' }, true)
// Manual redirect on the server (e.g., Next.js action/loader)
const { url } = await signIn('discord', { callbackUrl }, { baseUrl: process.env.AUTH_URL! }, 'manual')
return redirect(url)
// Programmatic fetch remains unchanged
await signIn('email-password', { email, password }, { basePath: '/api/auth' })
// Sign out with manual redirect
const out = await signOut({ callbackUrl: '/' }, { baseUrl: 'https://auth.example.com' }, 'manual')
Notes for proxy deployments:
- Ensure cookies used by the auth server are set with
SameSite=None; Securewhen the frontend runs on a different origin. - Add the frontend origin to your
AUTH_PROXY_BASE_URLSand CORS allowlist. - The client guarantees a CSRF cookie before producing a redirect URL by calling
getCsrfTokenwhen needed.
Server APIβ
Server helpers live under @robojs/auth/server (also re-exported from the root package). They let you normalise config, bridge Robo requests into Auth.js, and interact with storage/state.
Runtime + Routingβ
| Export | Description |
|---|---|
createAuthRequestHandler(config) | Wrap a prepared Auth.js config in a Robo-compatible handler. Perfect for custom HTTP routers or activities. |
configureAuthRuntime(config, options) | Warm a singleton Auth.js handler with explicit base path, cookie name, and secretβrequired before calling getServerSession or getToken. |
configureAuthProxyRuntime(options) | Point the helpers at a remote Auth.js deployment while keeping your local Robo routes. |
AUTH_ROUTES | Reference list of the REST routes the plugin wires up. Great for routing tables or documentation generators. |
DEFAULT_BASE_PATH | Literal /api/auth. Use when syncing config across services. |
Session Helpersβ
| Export | Description |
|---|---|
getServerSession(input?) | Invoke the Auth.js session route with the headers you provide and return the parsed Session (or null). Works with Request, Headers, or plain header records. |
getToken(input?, options?) | Extract the session token/JWT in the same way authjs/jwt does. Supports { raw: true } to return the cookie value instead of decoding. |
Configuration + Storageβ
| Export | Description |
|---|---|
normalizeAuthOptions(options) | Run your raw plugin config through the same defaults the CLI uses. Returns a NormalizedAuthPluginOptions object ready for Auth.js. |
authPluginOptionsSchema | Zod schema backing the plugin configuration. Useful for validation in external tooling. |
createFlashcoreAdapter(options) | Construct the built-in Flashcore adapter (requires secret). |
createPrismaAdapter(options) | Wrap Auth.js' Prisma adapter with Robo's password helpers, reset token hashing, and pagination helpers. |
listUsers(page?) / listUserIds(page?) | Paginate users or IDs stored through the Flashcore adapter. |
listPrismaUsers(options?) / listPrismaUserIds(options?) | Paginate Prisma-backed users or IDs using the shared return shape. |
authLogger | Namespaced logger instance (auth). |
Types & Utilitiesβ
| Export | Purpose |
|---|---|
getRequestPayload(request) | Parse (and cache) JSON/form bodies when writing custom route overrides or middleware. |
AuthPluginOptions | TypeScript type mirroring the plugin config shape. |
AuthEmailEvent, AuthMailer, MailMessage, EmailContext, TemplateConfig | Email-related types for advanced templating or mailer overrides. |
PasswordAdapter, PasswordRecord, assertPasswordAdapter | Contracts for credential-enabled adapters. |
PrismaAdapterOptions, PrismaAdapterModelOptions, PrismaClientLike | Types for wiring the Prisma adapter with custom model names or clients. |
Adapter, AdapterAccount, AdapterSession, AdapterUser, VerificationToken | Re-exported Auth.js adapter types for convenience. |
Email Delivery Optionsβ
You control how verification and transactional emails go out:
- Custom mailer β provide
emails.maileroremails.triggersentries that call straight into your favorite SDK (Resend, SES, SendGrid, etc.). - Auth.js mailer modules β point
emails.mailerto a module export{ module: 'resend', export: 'Resend' }so the plugin loads it for you at runtime. - React templates β attach
emails.templates['password:reset-request'] = { react: (ctx) => <MyEmail ctx={ctx} /> }and Robo will render it with@react-email/components+react-dom/serveron demand. - Disable defaults β set
emails.templates['user:created'] = false(or any other event) to suppress that automatic email without affecting the rest.
export default {
secret: process.env.AUTH_SECRET,
emails: {
mailer: { module: 'resend', export: 'Resend' },
triggers: {
'password:reset-request': (ctx) => ({
to: ctx.user.email!,
subject: 'Reset your password',
html: `<p>Hi ${ctx.user.name ?? 'friend'}, reset your password <a href="${ctx.links?.resetPassword}">here</a>.</p>`
})
},
templates: {
'user:created': {
subject: 'Welcome aboard!',
text: ({ user }) => `Hi ${user.name ?? 'there'}, thanks for joining Robo.`
}
}
}
}
If you provide both emails.mailer and a deliver function inside a trigger, the triggerβs deliver takes precedence for that event.
Request Payload Utilitiesβ
When you post JSON (for example, from a custom web UI) instead of relying on the built-in HTML forms, the getRequestPayload(request) helper lets you read and reuse the parsed body without exhausting the stream:
import { EmailPassword, getRequestPayload } from '@robojs/auth'
EmailPassword({
adapter,
authorize: async (credentials, ctx) => {
const payload = await getRequestPayload(ctx.request)
const body = payload.get<{ inviteCode?: string }>()
if (!body.inviteCode) return null
await verifyInvite(body.inviteCode)
payload.assign({ inviteCode: body.inviteCode.trim().toUpperCase() })
const user = await ctx.defaultAuthorize()
return user ? { ...user, inviteCode: body.inviteCode } : null
},
routes: {
signup: async ({ payload, defaultHandler }) => {
const body = payload.get<{ inviteCode?: string }>()
await verifyInvite(body.inviteCode)
payload.assign({ inviteCode: body.inviteCode?.trim()?.toUpperCase() })
return defaultHandler()
}
}
})
callbacks: {
async session({ session, token }) {
session.inviteCode = token.inviteCode
return session
}
},
events: {
async signIn(message) {
console.log('signIn', message.user?.id, message.session?.inviteCode)
}
}
async function verifyInvite(code?: string) {
if (!code) throw new Error('Missing invite')
// custom validation
}
getRequestPayload returns a RequestPayloadHandle with:
| Method | Description |
|---|---|
get<T>() | Returns the cached record. Provide a type argument for convenience. |
assign(partial) | Shallow-merges new fields into the cached payload (handy for normalising values). |
replace(data) | Overwrites the cached payload entirely. |
source | Reports where the data came from: 'json', 'form', or 'empty'. |
All email/password routes use this helper under the hood, so your overrides, the default handlers, and Auth.js callbacks all observe the same payload.
Email & Password Provider Extensionsβ
EmailPassword(options) is enabled by default and powers the classic email and password form flows (sign-in, sign-up, reset). It builds on Auth.js' Credentials provider but adds Robo niceties: shared payload parsing, CSRF checks, database session cookies, auto sign-in after signup, and configurable email templates.
Custom authorizeβ
EmailPassword({
adapter,
authorize: async (credentials, ctx) => {
const payload = await getRequestPayload(ctx.request)
const record = payload.get<{ email?: string; cliCode?: string }>()
if (!isCliCodeValid(record.cliCode)) {
return null
}
payload.assign({ cliCode: record.cliCode?.trim() })
const user = await ctx.defaultAuthorize()
return user ? { ...user, cliCode: record.cliCode } : null
}
})
authorize receives an EmailPasswordAuthorizeContext:
| Property | Description |
|---|---|
adapter | The active PasswordAdapter. |
request | The RoboRequest so you can inspect headers, IPs, or cookies. |
defaultAuthorize() | Runs the bundled credentials logic (findUserIdByEmail β verifyUserPassword). |
Return null to reject the login or an AdapterUser (optionally augmented with extra fields) to continue. Any additional fields you add can flow through JWT/session callbacks.
Route overridesβ
EmailPassword({
adapter,
routes: {
signup: async ({ payload, defaultHandler }) => {
const body = payload.get<{ inviteCode?: string }>()
await verifyInvite(body.inviteCode)
payload.assign({ inviteCode: body.inviteCode?.toUpperCase() })
return defaultHandler()
},
passwordResetRequest: async ({ payload, defaultHandler }) => {
auditResetAttempt(payload.get())
return defaultHandler()
},
passwordResetConfirm: async ({ request, defaultHandler }) => {
await enforcePasswordRules(await request.json())
return defaultHandler()
}
}
})
function auditResetAttempt(payload: Record<string, unknown>) {
console.log('password reset requested', payload.email)
}
async function enforcePasswordRules(body: Record<string, unknown>) {
if (typeof body.newPassword !== 'string' || body.newPassword.length < 12) {
throw new Error('Password must be at least 12 characters long')
}
}
Each override receives an EmailPasswordRouteContext with:
| Property | Description |
|---|---|
payload | Shared RequestPayloadHandle (see above). |
defaultHandler() | Invokes Robo's stock behaviour (CSRF checks, hashing, session cookies, email dispatch). |
adapter, authConfig, cookies, events, basePath, baseUrl, secret, sessionStrategy | Context from the running plugin instance. |
request | The raw RoboRequest. |
Available hooks:
| Hook | Triggered on | Typical use |
|---|---|---|
signup | POST /signup | Invite-code gating, analytics, custom redirects. |
passwordResetRequest | POST /password/reset/request | Captcha, throttling, SMS notifications. |
passwordResetConfirm | POST /password/reset/confirm / helper GET | Extra password policy, audit logging. |
Handlers can return their own Response to short-circuit the plugin or call defaultHandler() to inherit the stock flow. Because the payload is shared, any assign() calls you make persist through the rest of the lifecycle, including Auth.js callbacks and events.
Custom Password Hashingβ
By default, Robo.js uses argon2id for secure password hashing. You can swap this for your own algorithm (e.g. bcrypt, scrypt, or a legacy hash migration) by implementing the PasswordHasher interface.
import { EmailPassword } from '@robojs/auth'
import { compare, hash } from 'bcrypt'
import type { PasswordHasher } from '@robojs/auth/utils/password-hash'
class BcryptHasher implements PasswordHasher {
async hash(password: string): Promise<string> {
return hash(password, 10)
}
async verify(password: string, storedHash: string): Promise<boolean> {
return compare(password, storedHash)
}
needsRehash(storedHash: string): boolean {
// Optional: return true if you want to migrate legacy hashes on login
return false
}
}
export default <AuthPluginOptions>{
// ...
providers: [
EmailPassword({
hasher: new BcryptHasher()
})
]
}
The hasher is used for:
- Sign up: Hashing the password before storage.
- Sign in: Verifying the password against the stored hash.
- Password Reset: Hashing the new password.
- Rehashing: Automatically updating hashes if
needsRehashreturns true (e.g. upgrading parameters or algorithms).
Got questions?β
If you have any questions or need help with this plugin, join our Discord β weβre friendly and happy to help!
β π Community: Join our Discord server