Better-Auth Integration

Integrate @websideproject/nuxt-auto-api with better-auth for a complete authentication solution with sessions, OAuth, organizations, and more.

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

  1. Use better-auth plugins - Organizations, 2FA, OAuth, email verification, etc.
  2. Leverage organization roles - Use better-auth's built-in role system for multi-tenant apps
  3. Test with multiple roles - Write tests for each role and permission combination
  4. Monitor auth events - better-auth provides hooks for logging auth events
  5. 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
}

Need a Landing Page?

Modern landing pages with optional modules (blog, docs, forms, i18n). Let's discuss your project.

Build Your MVP

Full-stack SaaS development. Expert in database design, multi-tenancy, and scalable architecture.

Deployment Help

Dockerize your backend, set up CI/CD pipelines, deploy to Cloudflare or Hetzner. Early-stage setup.

Suggest a SaaS Tool

Missing a calculator or tool? Suggest what you'd like to see on our site.