superapp

better-auth

Set up the default authentication provider with better-auth, user table config, and session enrichment.

The betterAuthProvider is the recommended auth provider. It wraps better-auth and handles JWT verification, user lookup, and session management out of the box.

import { createEngine } from '@superapp/backend'
import { betterAuthProvider } from '@superapp/backend/auth/better-auth'

const auth = betterAuthProvider({
  secret: process.env.AUTH_SECRET!,
  userTable: {
    table: 'main.users',
    matchOn: { column: 'id', jwtField: 'id' },
  },
})

const engine = createEngine({
  connections: {
    main: { type: 'postgres', url: process.env.PG_URL! },
  },
  auth,
})

Full Configuration

import { betterAuthProvider } from '@superapp/backend/auth/better-auth'

const auth = betterAuthProvider({
  secret: process.env.AUTH_SECRET!,
  userTable: {
    table: 'main.users',
    matchOn: { column: 'id', jwtField: 'id' },
    activeCheck: { column: 'is_active', value: true },
    columns: ['id', 'email', 'name'],
  },
  resolveSession: async (user, db) => {
    const memberships = await db
      .selectFrom('main.members')
      .select(['organization_id', 'role'])
      .where('user_id', '=', user.id)
      .where('status', '=', 'active')
      .execute()

    return {
      ...user,
      org_ids: memberships.map(m => m.organization_id),
      org_roles: memberships,
      current_org_id: memberships[0]?.organization_id ?? null,
    }
  },
})

Options

OptionTypeRequiredDescription
secretstringYesSecret key for JWT signing and verification
userTable.tablestringYesFully qualified table name (e.g., main.users)
userTable.matchOn{ column, jwtField }YesMap JWT field to user table column
userTable.activeCheck{ column, value }NoOnly allow active users
userTable.columnsstring[]NoColumns to select from user table
resolveSession(user, db) => Promise<object>NoEnrich user with session data

User Table Matching

The matchOn config tells the provider how to find the user record from the JWT payload:

// JWT contains { id: 'usr_123', email: 'alice@example.com' }
matchOn: { column: 'id', jwtField: 'id' }
// → SELECT * FROM main.users WHERE id = 'usr_123'

Active Check

Block inactive or suspended users from making requests:

activeCheck: { column: 'is_active', value: true }
// → AND is_active = true

If the user record exists but fails the active check, the request returns 403 Forbidden.

Session Enrichment

The resolveSession callback runs after user lookup. Use it to attach organization memberships, roles, or any data your permissions reference:

resolveSession: async (user, db) => {
  const memberships = await db
    .selectFrom('main.members')
    .select(['organization_id', 'role'])
    .where('user_id', '=', user.id)
    .where('status', '=', 'active')
    .execute()

  return {
    ...user,
    org_ids: memberships.map(m => m.organization_id),
    current_org_id: memberships[0]?.organization_id ?? null,
  }
}

The returned object is available as $user.* in permissions. For example, $user.current_org_id references the current_org_id property.

On this page