Better-Auth Integration
Better-Auth Integration
Integrate @websideproject/nuxt-auto-api with better-auth for a complete authentication solution with sessions, OAuth, organizations, and more.
Note: This guide covers better-auth specifically. For core authorization concepts (permissions, listFilter, objectLevel, field-level), see Authentication & Authorization.
Installation
npm install better-auth
Basic Setup (Single-Tenant)
1. Configure Better-Auth
// server/lib/auth.ts
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { db } from '../database'
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'sqlite', // or 'pg', 'mysql'
}),
emailAndPassword: {
enabled: true,
},
})
2. Create Auth Plugin
// server/plugins/auth.ts
import { auth } from '../lib/auth'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('request', async (event) => {
const session = await auth.api.getSession({ headers: event.headers })
if (session) {
event.context.user = session.user
event.context.permissions = session.user.role ? [session.user.role] : []
}
})
})
3. Configure Authorization
// modules/base/auth.ts
export const usersAuth = {
permissions: {
read: ['user', 'admin'],
create: 'admin',
update: (context) => {
// Users can update their own profile
return context.user?.id === context.params.id || context.permissions.includes('admin')
},
delete: 'admin',
},
objectLevel: async (user, context) => {
// Object-level: can only access own user record
return user.id === context.user?.id || context.permissions.includes('admin')
},
}
Multi-Tenant Setup (with Organizations)
1. Enable Organization Plugin
// server/lib/auth.ts
import { betterAuth } from 'better-auth'
import { organization } from 'better-auth/plugins'
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'sqlite',
}),
plugins: [
organization({
// Organization plugin adds org support
})
],
})
2. Update Auth Plugin with Organization
// server/plugins/auth.ts
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('request', async (event) => {
const session = await auth.api.getSession({ headers: event.headers })
if (session) {
event.context.user = {
...session.user,
organizationId: session.activeOrganizationId, // From better-auth
}
event.context.permissions = session.user.role ? [session.user.role] : []
}
})
})
3. Configure Multi-Tenancy
// nuxt.config.ts
export default defineNuxtConfig({
autoApi: {
multiTenancy: {
enabled: true,
tenantIdField: 'organizationId',
getTenantId: (event) => {
return event.context.user?.organizationId
},
allowCrossTenantAccess: (user) => {
return user.role === 'superadmin'
},
},
},
})
4. Update Schemas
// server/database/schema.ts
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
organizationId: integer('organization_id').notNull(), // Auto-scoped
userId: integer('user_id').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
})
Role-Based Permissions
Define Roles
// server/database/schema.ts
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
email: text('email').notNull().unique(),
name: text('name'),
role: text('role', {
enum: ['user', 'editor', 'admin', 'superadmin']
}).default('user'),
})
Configure Permissions
// modules/base/auth.ts
export const postsAuth = {
permissions: {
read: ['user', 'editor', 'admin'], // Anyone can read
create: ['editor', 'admin'], // Editors and admins can create
update: (context) => {
// Owner or admin can update
return context.user?.id === context.params.userId ||
context.permissions.includes('admin')
},
delete: 'admin', // Only admins can delete
},
}
Authorization Configuration
Configure authorization using the same patterns as any auth system. See the Authorization guide for detailed documentation on:
- Operation-level permissions
- SQL-level list filtering (
listFilter) - Object-level authorization (
objectLevel) - Field-level authorization
Example with better-auth roles:
// modules/blog/auth.ts
import { eq } from 'drizzle-orm'
export const articlesAuth = {
permissions: {
read: () => true, // Public read access
create: ['editor', 'admin'],
update: ['editor', 'admin'],
delete: 'admin',
},
listFilter: (table, ctx) => {
// Editors/admins see all, others see only published
if (ctx.user?.role === 'admin' || ctx.user?.role === 'editor') {
return undefined
}
return eq(table.published, true)
},
objectLevel: async (article, ctx) => {
if (ctx.user?.role === 'admin') return true
if (ctx.operation === 'get') {
return article.published || ctx.user?.role === 'editor'
}
return ctx.user?.role === 'editor'
},
}
Field-Level Authorization
Hide sensitive fields based on role:
export const usersAuth = {
fields: {
email: {
read: (context) => {
// Can only read own email or if admin
return context.user?.id === context.params.id ||
context.permissions.includes('admin')
},
},
role: {
write: 'admin', // Only admins can change roles
},
},
}
Frontend Auth
Setup Better-Auth Client
// plugins/auth.client.ts
import { createAuthClient } from 'better-auth/vue'
export default defineNuxtPlugin(() => {
const authClient = createAuthClient({
baseURL: 'http://localhost:3000',
})
return {
provide: {
auth: authClient,
},
}
})
Use in Components
<script setup lang="ts">
const { $auth } = useNuxtApp()
const user = $auth.useSession()
// Protected API calls automatically include auth headers
const { data: posts } = usePosts()
</script>
<template>
<div v-if="user">
<h1>Welcome, {{ user.name }}</h1>
<div v-for="post in posts" :key="post.id">
{{ post.title }}
</div>
</div>
<div v-else>
<button @click="$auth.signIn.email(...)">Sign In</button>
</div>
</template>
Organization Switching
With better-auth organizations:
<script setup lang="ts">
const { $auth } = useNuxtApp()
const organizations = $auth.useOrganizations()
const switchOrg = async (orgId: string) => {
await $auth.organization.setActive(orgId)
// Refetch data - will now show data from new org
await refetch()
}
</script>
Testing with Auth
// tests/api.test.ts
import { createTestContext } from '#test-utils'
test('user can only see own posts', async () => {
const ctx = createTestContext({
user: { id: 1, organizationId: 1, role: 'user' }
})
const posts = await getPosts(ctx)
// All posts should belong to user's org
expect(posts.every(p => p.organizationId === 1)).toBe(true)
})
test('admin can see all posts', async () => {
const ctx = createTestContext({
user: { id: 1, organizationId: 1, role: 'superadmin' }
})
const posts = await getPosts(ctx)
// Admin can see posts from all orgs
expect(posts.some(p => p.organizationId !== 1)).toBe(true)
})
Best Practices
- Use better-auth plugins - Organizations, 2FA, OAuth, email verification, etc.
- Leverage organization roles - Use better-auth's built-in role system for multi-tenant apps
- Test with multiple roles - Write tests for each role and permission combination
- Monitor auth events - better-auth provides hooks for logging auth events
- Session management - Configure appropriate session timeouts and refresh strategies
For general authorization best practices (listFilter vs objectLevel, role patterns, etc.), see the Authorization guide.
Common Patterns
Owner or Admin
permissions: {
update: (context) => {
return context.user?.id === context.params.userId ||
context.permissions.includes('admin')
}
}
Organization Member
objectLevel: async (record, context) => {
return record.organizationId === context.user?.organizationId ||
context.permissions.includes('superadmin')
}
Public Read, Authenticated Write
permissions: {
read: true, // Anyone
create: ['user', 'admin'], // Authenticated only
}
Authentication & Authorization
@websideproject/nuxt-auto-api provides a flexible multi-tier authorization system that works with any authentication provider. You control user context and permissions, while the module enforces authorization rules.
Nested Relations
Nuxt Auto API supports advanced nested relation loading with field selection, filtering, and pagination at each level of nesting.