• Start

Authentication

/

Better Auth

Plugins

Use any Better Auth plugin with the SurrealDB adapter and call the SurrealQL helper functions generated for the organisation plugin.

The @surrealdb/better-auth adapter works with all Better Auth plugins. Pass them in the plugins array of your Better Auth configuration:

import { betterAuth } from 'better-auth';
import { organization } from 'better-auth/plugins/organization';
import { twoFactor } from 'better-auth/plugins/two-factor';
import { admin } from 'better-auth/plugins/admin';
import { Surreal } from 'surrealdb';
import { surrealAdapter } from '@surrealdb/better-auth';

const db = new Surreal();
await db.connect('ws://localhost:8000/rpc');
await db.use({ namespace: 'myapp', database: 'production' });

export const auth = betterAuth({
    database: surrealAdapter({ db }),
    plugins: [
        organization(),
        twoFactor(),
        admin(),
    ],
});

A wider example combining several plugins:

import { betterAuth } from 'better-auth';
import { admin } from 'better-auth/plugins/admin';
import { anonymous } from 'better-auth/plugins/anonymous';
import { bearer } from 'better-auth/plugins/bearer';
import { magicLink } from 'better-auth/plugins/magic-link';
import { multiSession } from 'better-auth/plugins/multi-session';
import { organization } from 'better-auth/plugins/organization';
import { twoFactor } from 'better-auth/plugins/two-factor';
import { username } from 'better-auth/plugins/username';
import { Surreal } from 'surrealdb';
import { surrealAdapter } from '@surrealdb/better-auth';

const db = new Surreal();
await db.connect('ws://localhost:8000/rpc');
await db.use({ namespace: 'myapp', database: 'production' });

export const auth = betterAuth({
    secret: process.env.BETTER_AUTH_SECRET!,
    baseURL: process.env.BETTER_AUTH_URL ?? 'http://localhost:3000',
    emailAndPassword: { enabled: true },
    database: surrealAdapter({ db }),
    plugins: [
        organization({
            teams: { enabled: true },
            dynamicAccessControl: { enabled: true },
        }),
        twoFactor(),
        admin(),
        bearer(),
        username(),
        magicLink({
            sendMagicLink: async ({ email, url }) => {
                // send an email containing the magic link url
            },
        }),
        anonymous(),
        multiSession(),
    ],
});
Important

Each plugin may add new tables. Re-run schema generation after adding plugins to produce the updated DDL, or use schemaMode: 'schemaless' to accept the new fields without regenerating.

When you use the organization plugin (with or without the teams option), schema generation also emits a set of fn::* SurrealQL functions you can call directly in your own queries, rules, and permissions. These functions are defined with IF NOT EXISTS, so re-running the generated schema is safe.

These functions are emitted when the organization plugin is active.

FunctionSignatureReturnsDescription
fn::auth::organization::member_of(userId: string, organizationId: string)boolTrue if the user is a member of the organisation.
fn::auth::organization::get_role(userId: string, organizationId: string)option<string>The member's role ("owner", "admin", "member"), or NONE if not a member.
fn::auth::organization::has_role(userId: string, organizationId: string, minRole: string)boolTrue if the user's role is equal to or senior to minRole (owner > admin > member).
fn::auth::organization::members(organizationId: string)arrayAll members records for the organisation.
fn::auth::organization::teams(organizationId: string)arrayAll teams records for the organisation (requires teams: { enabled: true }).
fn::auth::organization::has_permission(userId: string, organizationId: string, resource: string, action: string)boolTrue if the user's role has a custom permission for the given resource and action (requires dynamicAccessControl).
-- Check if a user belongs to an organisation
IF fn::auth::organization::member_of($userId, $organizationId) {
    -- allow access
};

-- Require at least admin-level access
IF fn::auth::organization::has_role($userId, $organizationId, "admin") {
    -- perform a privileged operation
};

-- Get a user's role string
LET $role = fn::auth::organization::get_role($userId, $organizationId);

-- List all members
LET $members = fn::auth::organization::members($organizationId);

These functions are emitted when teams: { enabled: true } is set on the organization plugin.

FunctionSignatureReturnsDescription
fn::auth::team::member_of(userId: string, teamId: string)boolTrue if the user is a member of the team.
fn::auth::team::members(teamId: string)arrayAll teamMembers records for the team.
-- Check team membership
IF fn::auth::team::member_of($userId, $teamId) {
    -- allow team-scoped access
};

-- List all team members
LET $members = fn::auth::team::members($teamId);

Was this page helpful?