Skip to content

Adding Organizations feature

Better Auth, which we already use for authentication in this boilerplate, offers a complete plugin system to extend its features.

The library provides a specific plugin for managing organizations. The official Better Auth documentation covers this plugin and all its options in depth.

This guide will walk you through the process of quickly configuring the Organizations plugin in the boilerplate with NestJS and MikroORM.

Since the Organizations plugin is part of Better Auth and works as an integrated system, we will add all organization-related files in the existing /auth folder.

Here is the file structure we will modify:

apps/api/src/
├── auth/
│ └── auth.entity.ts (to update)
└── better-auth.config.ts (to update)

  1. Add the plugin to Better Auth configuration

    Section titled “Add the plugin to Better Auth configuration”

    In your Better Auth configuration file better-auth.config.ts on the API side, add the organization plugin:

    import { organization } from "better-auth/plugins"
    export const auth = betterAuth({
    // ... your existing config
    plugins: [
    organization() // Minimal configuration
    ]
    })

  2. The Organizations plugin requires 3 mandatory tables: Organization, Member, and Invitation. There are also optional tables depending on your needs.

    The official Better Auth schema details all tables and their fields.

    Add the 3 organization entities to your existing auth.entity.ts file:

    src/auth/auth.entity.ts
    // ... your existing entities (User, Session, etc.)
    /**
    * Store organization information
    */
    @Entity({ tableName: 'organization' })
    export class Organization {
    @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
    id!: string;
    @Property()
    name!: string;
    @Property({ nullable: true })
    slug?: string;
    @Property({ nullable: true })
    logo?: string;
    @Property({ nullable: true })
    metadata?: string;
    @Property({ fieldName: 'createdAt' })
    createdAt: Date = new Date();
    @OneToMany(() => Member, (member) => member.organization)
    members = new Collection<Member>(this);
    @OneToMany(() => Invitation, (invitation) => invitation.organization)
    invitations = new Collection<Invitation>(this);
    }
    /**
    * Store members of an organization
    */
    @Entity({ tableName: 'member' })
    export class Member {
    @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
    id!: string;
    @ManyToOne(() => User, { fieldName: 'userId', deleteRule: 'cascade' })
    user!: User;
    @ManyToOne(() => Organization, { fieldName: 'organizationId' })
    organization!: Organization;
    @Property({ default: 'member' })
    role = 'member';
    @Property({ fieldName: 'createdAt' })
    createdAt: Date = new Date();
    }
    /**
    * Store invitations to join an organization
    */
    @Entity({ tableName: 'invitation' })
    export class Invitation {
    @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
    id!: string;
    @Property()
    email!: string;
    @ManyToOne(() => User, { fieldName: 'inviterId' })
    inviter!: User;
    @ManyToOne(() => Organization, { fieldName: 'organizationId' })
    organization!: Organization;
    @Property()
    role!: string;
    @Property()
    status!: string;
    @Property({ fieldName: 'expiresAt' })
    expiresAt!: Date;
    }
    src/auth/auth.entity.ts
    // ... your existing entities
    @Entity(tableName: 'user')
    export class User {
    // ... your existing properties
    @OneToMany(() => Member, member => member.user)
    members = new Collection<Member>(this)
    }

    The Better Auth Organizations plugin automatically adds the activeOrganizationId field to the session when a user logs in or switches their active organization. You need to add this field to your MikroORM Session entity to match the database schema:

    src/auth/auth.entity.ts
    // ... your existing entities
    @Entity(tableName: 'session')
    export class Session {
    // ... your existing properties
    @Property({ fieldName: 'activeOrganizationId', nullable: true })
    activeOrganizationId?: string;
    }

  3. Now that you have added the entities, you need to update your database to create the corresponding tables.

    Depending on your project stage, you can either recreate your database from scratch if you’re in development without important data, or use the migration system to preserve existing data.


  4. Now that everything is configured on the API side, you can use organization-related methods from your frontend client.

    Usage examples:

    // Create an organization
    const { data, error } = await authClient.organization.create({
    name: "My Organization",
    slug: "my-org",
    logo: "https://example.com/logo.png",
    })

The Organizations plugin is now configured and operational. You can create organizations, invite members, and manage roles via the Better Auth client.

Better Auth provides a complete set of methods to interact with organization entities (create, update, delete, invite members, etc.).

However, you may need to implement custom business logic specific to your application. In this case, you can create your own methods in auth.service.ts to handle organization-related operations that go beyond what Better Auth provides out of the box.

Example:

src/auth/organization.service.ts
import { Property } from '@hubspot/api-client/lib/codegen/crm/properties'
import { EnsureRequestContext, EntityManager, wrap } from '@mikro-orm/core'
import { Injectable } from '@nestjs/common'
import { Crm } from '../crm/crm.contract'
import { Member, Organization, User } from './auth.entity'
@Injectable()
export class OrganizationService {
constructor(private readonly em: EntityManager) {}
@EnsureRequestContext()
async getOrganizationById(organizationId: string): Promise<Organization | null> {
return this.em.findOne(Organization, { id: organizationId })
}
@EnsureRequestContext()
async getOrganizationUsers(organizationId: string): Promise<User[]> {
return this.em.find(User, { members: { organization: { id: organizationId } } })
}
@EnsureRequestContext()
async getUserActiveOrganization(userId: string): Promise<Organization | null> {
const member = await this.em.findOne(Member, {
user: {
id: userId,
},
}, { populate: ['organization'] })
if (!member || !member.organization)
return null
return member.organization
}
}

The boilerplate already provides authentication decorators and guards. You can extend them to handle organization-specific logic.

Add organization-related decorators:

For example, if you have a resource linked to an organization, and want to create a myResource.guard to check if the user is a member of the organization, you can do the following:

src/auth/myResource.guard.ts
import { EntityManager } from '@mikro-orm/postgresql'
import {
CanActivate,
ExecutionContext,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common'
import { LoggedInBetterAuthSession } from '../../../config/better-auth.config'
import { ErrorCode } from '../../../utils/error-handling'
import { Client } from '../../client/client.entity'
import { Proposal } from '../entities/proposal.entity'
// Guard to check if the client exists and belongs to the organization
@Injectable()
export class MyResourceGuard implements CanActivate {
constructor(private readonly em: EntityManager) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest()
const clientSlug: string = request.params.clientSlug
const session = request.session as LoggedInBetterAuthSession
const organizationId = session.session.activeOrganizationId
if (!organizationId) {
throw new UnauthorizedException(ErrorCode.UNAUTHORIZED)
}
const resourceId = request.params.resourceId
if (resourceId) {
const resource = await this.em.findOne(Resource, { id: resourceId, organization: { id: organizationId } })
if (!resource) {
throw new NotFoundException(ErrorCode.NOT_FOUND)
}
}
return true
}
}

You can add custom fields to the organization entities to match your business needs. Since Better Auth v1.3, custom fields added via additionalFields will automatically be available in the API endpoints.

See the official Better Auth documentation for more details, don’t forget to update your entities to match the new schema.