Skip to main content

@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.

GitHub
license npm install
size Discord All
Contributors

➞ πŸ“š Documentation: Getting started

➞ πŸš€ Community: Join our Discord server

Installation​

Install and register the plugin:

Terminal
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:

OptionDefaultNotes
basePath/api/authPrefix for generated routes.
appName'Robo.js'Display name injected into default emails and available as ctx.appName.
secretrequired in prodUsed for JWT + token hashing. Reads AUTH_SECRET/NEXTAUTH_SECRET.
urlenv (AUTH_URL/NEXTAUTH_URL) or http://localhost:3000Canonical URL for Auth.js callbacks.
redirectProxyUrlAUTH_REDIRECT_PROXY_URLUseful for preview deployments.
providers[]Array of Auth.js providers. Helpers live under @robojs/auth/providers.
adapterFlashcoreSwap for any Auth.js adapter when you outgrow the default storage.
session.strategyjwt (or database when adapter present)Supports maxAge & updateAge.
cookies, callbacks, events, pagesAuth.js defaultsUse the same shapes as upstream Auth.js.
email / emails{}Override templates, configure mailers, or bind to third-party transports.
upstreamunsetForward 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:

  1. Sending a callbackUrl in the signup request body.
  2. Setting pages.newUser in 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 – your PrismaClient instance.
  • secret – standard Auth.js secret for session/token helpers (typically AUTH_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.

FunctionDescription
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 pass headers or a custom fetch)
    • 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 performs window.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; Secure when the frontend runs on a different origin.
  • Add the frontend origin to your AUTH_PROXY_BASE_URLS and CORS allowlist.
  • The client guarantees a CSRF cookie before producing a redirect URL by calling getCsrfToken when 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​

ExportDescription
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_ROUTESReference list of the REST routes the plugin wires up. Great for routing tables or documentation generators.
DEFAULT_BASE_PATHLiteral /api/auth. Use when syncing config across services.

Session Helpers​

ExportDescription
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​

ExportDescription
normalizeAuthOptions(options)Run your raw plugin config through the same defaults the CLI uses. Returns a NormalizedAuthPluginOptions object ready for Auth.js.
authPluginOptionsSchemaZod 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.
authLoggerNamespaced logger instance (auth).

Types & Utilities​

ExportPurpose
getRequestPayload(request)Parse (and cache) JSON/form bodies when writing custom route overrides or middleware.
AuthPluginOptionsTypeScript type mirroring the plugin config shape.
AuthEmailEvent, AuthMailer, MailMessage, EmailContext, TemplateConfigEmail-related types for advanced templating or mailer overrides.
PasswordAdapter, PasswordRecord, assertPasswordAdapterContracts for credential-enabled adapters.
PrismaAdapterOptions, PrismaAdapterModelOptions, PrismaClientLikeTypes for wiring the Prisma adapter with custom model names or clients.
Adapter, AdapterAccount, AdapterSession, AdapterUser, VerificationTokenRe-exported Auth.js adapter types for convenience.

Email Delivery Options​

You control how verification and transactional emails go out:

  • Custom mailer – provide emails.mailer or emails.triggers entries that call straight into your favorite SDK (Resend, SES, SendGrid, etc.).
  • Auth.js mailer modules – point emails.mailer to 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/server on 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:

MethodDescription
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.
sourceReports 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:

PropertyDescription
adapterThe active PasswordAdapter.
requestThe 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:

PropertyDescription
payloadShared RequestPayloadHandle (see above).
defaultHandler()Invokes Robo's stock behaviour (CSRF checks, hashing, session cookies, email dispatch).
adapter, authConfig, cookies, events, basePath, baseUrl, secret, sessionStrategyContext from the running plugin instance.
requestThe raw RoboRequest.

Available hooks:

HookTriggered onTypical use
signupPOST /signupInvite-code gating, analytics, custom redirects.
passwordResetRequestPOST /password/reset/requestCaptcha, throttling, SMS notifications.
passwordResetConfirmPOST /password/reset/confirm / helper GETExtra 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 needsRehash returns 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

More on GitHub​