Skip to content

Auth Plugin

The Auth Plugin provides a complete authentication and user management system for Nevr applications. It includes features like email/password sign-up and sign-in, session management, OAuth social logins, password reset, email verification, account linking, and more.

Why Use Auth Plugin?

ChallengeWithout NevrWith Nevr Auth
Password hashingImplement bcrypt/argon2Automatic PBKDF2
Session tokensBuild token systemSecure sessions built-in
OAuth flowPKCE, state, callbacksOne config object
Account securityBuild from scratchFresh sessions, rate limits

Installation

Set your secret

Generate a high-entropy secret (minimum 32 characters):

bash
# Generate a secure secret
openssl rand -base64 32

Add it to your .env file:

bash
NEVR_AUTH_SECRET="paste-your-generated-secret-here"

Security

The secret must be at least 32 characters. It's used for HMAC-SHA256 signing of sessions and tokens. Never commit it to version control.

Add the auth plugin

typescript
// nevr.config.ts
import { defineConfig } from "nevr"
import { auth } from "nevr/plugins/auth"

export const config = defineConfig({
  database: "sqlite",
  entities: [],
  plugins: [
    auth({
      emailAndPassword: { enabled: true },
    }),
  ],
})

export default config

The plugin reads NEVR_AUTH_SECRET (or AUTH_SECRET) from your environment automatically — no need to pass secret in the options.

Create the server with getUser

The auth plugin creates sessions in the database. To resolve the authenticated user on each request, pass sessionAuth() as the getUser callback in your adapter.

typescript
// src/server.ts
import "dotenv/config"
import express from "express"
import { PrismaClient } from "@prisma/client"
import { nevr } from "nevr"
import { prisma } from "nevr/drivers/prisma"
import { expressAdapter, sessionAuth } from "nevr/adapters/express"
import { config } from "./nevr.config.js"

const db = new PrismaClient()
const driver = prisma(db)

const api = nevr({ ...config, driver })

const app = express()
app.use(express.json())
app.use("/api", expressAdapter(api, {
  getUser: sessionAuth(driver),
}))

app.listen(3000)
typescript
// src/server.ts
import "dotenv/config"
import { Hono } from "hono"
import { serve } from "@hono/node-server"
import { PrismaClient } from "@prisma/client"
import { nevr } from "nevr"
import { prisma } from "nevr/drivers/prisma"
import { honoAdapter, sessionAuth } from "nevr/adapters/hono"
import { config } from "./nevr.config.js"

const db = new PrismaClient()
const driver = prisma(db)

const api = nevr({ ...config, driver })

const app = new Hono()
app.route("/api", honoAdapter(api, {
  getUser: sessionAuth(driver),
}))

serve({ fetch: app.fetch, port: 3000 })
typescript
import { nevr } from "nevr"
import { prisma } from "nevr/drivers/prisma"
import { PrismaClient } from "@prisma/client"
import { nextCookies } from "nevr/adapters/nextjs"
import { config } from "./nevr.config"

const db = new PrismaClient()
const driver = prisma(db)

export const api = nevr({
  ...config,
  driver,
  plugins: [...(config.plugins ?? []), nextCookies()],
})
typescript
// app/api/[...nevr]/route.ts
import { toNextHandler, sessionAuth } from "nevr/adapters/nextjs"
import { api } from "@/lib/nevr"

export const { GET, POST, PUT, PATCH, DELETE } = toNextHandler(api, {
  getUser: sessionAuth(api),
})

sessionAuth() reads the nevr.session_token cookie (or Authorization: Bearer header), finds the session in the database, validates expiry, and returns the user. Without it, req.user is always null and rules like "authenticated" or "owner" will deny access.

Generate and push or migrate the database

bash
npx nevr generate    # Generates user + session tables
npx nevr db:push     # Push to database
# or
npx nevr db:migrate  # Create migration files

Client Setup

typescript
import { createClient } from "nevr/client"
import { authClient } from "nevr/plugins/auth/client"
import type { API } from "./api"

const client = createClient<API>()({
  baseURL: "/api",
  plugins: [authClient()],
})

Configuration Reference

Core Options

OptionTypeDefaultDescription
secretstringNEVR_AUTH_SECRET or AUTH_SECRET envRequired. Min 32 chars. Signing key for tokens/sessions. Auto-reads from env.
baseURLstring-Base URL for OAuth callbacks

Email & Password

typescript
auth({
  emailAndPassword: {
    enabled: true,
    minPasswordLength: 8,
    maxPasswordLength: 128,
    autoSignIn: true,
    requireEmailVerification: false,
    disableSignUp: false,
    resetPasswordTokenExpiresIn: 3600,
    revokeSessionsOnPasswordReset: false,
    sendResetPassword: async ({ user, url, token }) => {
      await sendEmail(user.email, `Reset: ${url}`)
    },
    onPasswordReset: async ({ user }) => {
      console.log(`${user.email} reset password`)
    },
  },
})
OptionTypeDefaultDescription
enabledbooleantrueEnable email/password auth
minPasswordLengthnumber8Minimum password characters
maxPasswordLengthnumber128Maximum password characters
autoSignInbooleantrueAuto-signin after signup
requireEmailVerificationbooleanfalseBlock signin until verified
disableSignUpbooleanfalseDisable new registrations
resetPasswordTokenExpiresInnumber3600Reset token TTL (seconds)
revokeSessionsOnPasswordResetbooleanfalseLogout all on password change
sendResetPasswordfunction-Custom reset email sender
onPasswordResetfunction-Post-reset callback

Session Options

typescript
auth({
  session: {
    expiresIn: 60 * 60 * 24 * 7,  // 7 days
    updateAge: 60 * 60 * 24,      // Refresh after 1 day
    freshAge: 300,                // 5 min for sensitive ops
    cookieName: "nevr.session_token",
    cookie: {
      secure: true,
      httpOnly: true,
      sameSite: "lax",
      path: "/",
      domain: undefined,
    },
  },
})
OptionTypeDefaultDescription
expiresInnumber604800 (7d)Session TTL in seconds
updateAgenumber86400 (1d)Refresh threshold in seconds
freshAgenumber-"Fresh" session window for sensitive ops
cookieNamestringnevr.session_tokenSession cookie name
cookie.securebooleantrue in prodHTTPS only
cookie.httpOnlybooleantrueNo JS access
cookie.sameSitestringlaxCSRF protection
cookie.pathstring/Cookie path
cookie.domainstring-Cookie domain

Email Verification

typescript
auth({
  emailVerification: {
    enabled: true,
    expiresIn: 3600,
    sendOnSignUp: true,
    autoSignInAfterVerification: true,
    sendVerificationEmail: async ({ user, url, token }) => {
      await sendEmail(user.email, `Verify: ${url}`)
    },
    onEmailVerification: async (user) => {
      console.log(`${user.email} verified`)
    },
  },
})

Password Hashing

typescript
auth({
  password: {
    cost: 100000,  // PBKDF2 iterations
    hash: async (password, cost) => customHash(password),
    verify: async (password, hash) => customVerify(password, hash),
  },
})

OAuth Providers

typescript
auth({
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      scope: ["email", "profile"],
      accessType: "offline",  // Get refresh token
      hd: "company.com",      // Restrict to domain
    },
    github: {
      clientId: process.env.GITHUB_CLIENT_ID,
      clientSecret: process.env.GITHUB_CLIENT_SECRET,
      scope: ["user:email"],
      allowSignup: true,
    },
    apple: {
      clientId: process.env.APPLE_SERVICE_ID,
      clientSecret: process.env.APPLE_SECRET,
      appBundleIdentifier: "com.yourapp",
    },
  },
})

Account Linking

typescript
auth({
  account: {
    accountLinking: {
      enabled: true,
      trustedProviders: ["google"],  // Auto-link these providers
      allowDifferentEmails: false,
    },
  },
})

Extending User Schema

typescript
auth({
  user: {
    additionalFields: {
      phone: { type: "string", required: false, input: true, returned: true },
      role: { type: "string", required: false, input: false, returned: true },
      premium: { type: "boolean", required: false, input: false, returned: true },
    },
  },
})

Generated Entities

User

FieldTypeDescription
idstringUUID
emailstringUnique, lowercase, trimmed
namestringDisplay name
emailVerifiedbooleanVerification status
imagestring?Avatar URL
createdAtDateCreation timestamp
updatedAtDateLast update

Session

FieldTypeDescription
idstringUUID
tokenstringSecure session token (hidden)
userIdstringUser reference
expiresAtDateExpiration time
ipAddressstring?Client IP
userAgentstring?Browser info
createdAtDateCreation time

Account

FieldTypeDescription
idstringUUID
userIdstringUser reference
providerIdstring"credential", "google", "github", etc.
accountIdstringProvider user ID
passwordstring?Hashed password (credential only)
accessTokenstring?OAuth access token (hidden)
refreshTokenstring?OAuth refresh token (hidden)
expiresAtDate?Token expiration
scopestring?Granted scopes

Verification

FieldTypeDescription
idstringUUID
identifierstringPurpose (email, password-reset)
valuestringToken value (hidden)
expiresAtDateToken expiration

API Endpoints

Authentication

MethodPathDescription
POST/auth/sign-up/emailCreate account with email/password
POST/auth/sign-in/emailSign in with email/password
POST/auth/sign-outSign out (clear session)

Session Management

MethodPathDescription
GET/auth/sessionGet current session + user
GET/auth/list-sessionsList all user sessions
POST/auth/revoke-sessionRevoke specific session
POST/auth/revoke-sessionsRevoke all sessions
POST/auth/revoke-other-sessionsRevoke all except current

Email Verification

MethodPathDescription
POST/auth/send-verification-emailSend verification email
GET/auth/verify-emailVerify email token

Password Reset

MethodPathDescription
POST/auth/request-password-resetRequest reset email
GET/auth/reset-password/callbackVerify reset token
POST/auth/reset-passwordSet new password

User Management

MethodPathDescription
POST/auth/update-userUpdate name, image, etc.
POST/auth/change-passwordChange password (requires current)
POST/auth/change-emailChange email (sends verification)
POST/auth/delete-userDelete account permanently

Account Management

MethodPathDescription
GET/auth/list-accountsList linked providers
POST/auth/unlink-accountUnlink a provider

OAuth

MethodPathDescription
POST/auth/sign-in/:providerStart OAuth flow
GET/auth/callback/:providerHandle OAuth callback
POST/auth/link-socialLink OAuth to existing account

Client Methods

typescript
import { createClient } from "nevr/client"
import { authClient } from "nevr/plugins/auth/client"
import type { API } from "./server/api"

// Use curried pattern for full type inference
const client = createClient<API>()({
  baseURL: "http://localhost:3000",
  basePath: "/api",
  plugins: [authClient()],
})

// Authentication
await client.auth.signUp.email({ email, password, name })
await client.auth.signIn.email({ email, password })
await client.auth.signOut()

// Session
const { user, session } = await client.auth.getSession()
const sessions = await client.auth.listSessions()
await client.auth.revokeSession({ token })
await client.auth.revokeOtherSessions()

// Profile
await client.auth.updateUser({ name: "New Name" })
await client.auth.changePassword({ currentPassword, newPassword })
await client.auth.changeEmail({ newEmail })
await client.auth.deleteUser({ password })

// Accounts
const { accounts } = await client.auth.listAccounts()
await client.auth.unlinkAccount({ providerId: "google" })

// OAuth
const { url } = await client.auth.signIn.social({ provider: "google" })
await client.auth.linkSocial({ providerId: "github" })

Hooks

Register callbacks for auth events:

typescript
import { registerAuthHooks } from "nevr/plugins/auth"

registerAuthHooks({
  beforeCreateUser: async (data) => {
    data.role = "member"
    return data
  },
  afterCreateUser: async (user) => {
    await sendWelcomeEmail(user.email)
  },
  beforeSignIn: async (email) => {
    // Rate limiting, etc.
  },
  afterSignIn: async (user, session) => {
    await logSignIn(user.id, session.ipAddress)
  },
  beforeCreateSession: async (userId) => {},
  afterCreateSession: async (session) => {},
  beforeSignOut: async (userId) => {},
  afterSignOut: async (userId) => {},
  afterEmailVerified: async (user) => {},
})

Error Codes

CodeMessage
USER_ALREADY_EXISTSEmail already registered
INVALID_EMAILInvalid email format
PASSWORD_TOO_SHORTPassword below minimum length
PASSWORD_TOO_LONGPassword above maximum length
INVALID_EMAIL_OR_PASSWORDWrong credentials
EMAIL_NOT_VERIFIEDEmail verification required
ACCOUNT_NOT_FOUNDNo account found
CREDENTIAL_ACCOUNT_NOT_FOUNDNo password set
SESSION_NOT_FOUNDInvalid session
SESSION_EXPIREDSession has expired
SESSION_NOT_FRESHSession too old for sensitive op
VERIFICATION_TOKEN_EXPIREDToken has expired
INVALID_VERIFICATION_TOKENInvalid token
UNAUTHORIZEDNot authenticated
FORBIDDENNot authorized

Rate Limiting

Built-in rate limiting protects against brute-force and credential stuffing attacks. Fully configurable per endpoint type.

Configuration

typescript
auth({
  // Default rate limiting (enabled by default)
  rateLimit: {
    signIn: { window: 60000, max: 10 },      // 10/min
    signUp: { window: 60000, max: 10 },      // 10/min
    passwordReset: { window: 60000, max: 5 }, // 5/min
    emailVerification: { window: 60000, max: 5 },
  },
})

// Stricter limits for high-security apps
auth({
  rateLimit: {
    signIn: { window: 60000, max: 5 },       // 5/min
    signUp: { window: 300000, max: 3 },      // 3/5min
    passwordReset: { window: 60000, max: 3 },
  },
})

// Disable rate limiting (use external WAF/Cloudflare)
auth({
  rateLimit: false,
})

Default Limits

Endpoint TypeWindowMax Requests
signIn60s10
signUp60s10
passwordReset60s5
emailVerification60s5

Security Features

  • PKCE for OAuth (RFC 7636)
  • AES-256-GCM encrypted state
  • PBKDF2 password hashing (100k iterations)
  • Fresh sessions for sensitive operations
  • Automatic token expiry (10 min for OAuth state)
  • Session binding to IP/User-Agent

Auth Sub-Plugins

Extend authentication with additional capabilities:

Other Auth Guides

Released under the MIT License.