Organization Plugin Configuration

Overview

The Better Auth Organization Plugin provides the framework for multi-tenant applications. In the Gremlin monorepo, it manages organization lifecycle (creation, deletion), member invitations, and organization-specific roles (owner, admin, staff, viewer).

Configuration

The plugin is initialized in the DIContainer alongside the other Better Auth components.

Location

backend/bun/apps/monolith/src/config/di-container.ts

Implementation

import { organization } from 'better-auth/plugins';

this.auth = betterAuth({
  // ... other config
  plugins: [
    organization(), // Default configuration
  ],
});
The plugin manages the following database tables:
  • organizations: Metadata for each organization (name, slug, image).
  • members: Join table connecting users to organizations with a specific role.
  • invitations: Pending requests for users to join organizations.

Core Features

1. Organization Roles

Every member of an organization is assigned a role. These roles are used by the Go backend to resolve granular permissions.
RoleDescription
ownerFull control over the organization, including billing and deletion.
adminCan manage members, invitations, and settings.
staffCan perform operational tasks (e.g., approving leave requests).
viewerRead-only access to organization data.

2. Member Management Endpoints

The Bun backend exposes these endpoints via the Better Auth API:
  • POST /api/auth/organization/create: Create a new organization (caller becomes owner).
  • POST /api/auth/organization/invite: Invite a user via email.
  • POST /api/auth/organization/update-member-role: Change a member’s role.
  • POST /api/auth/organization/remove-member: Kick a member from the org.

3. JWT & Context Integration

The user’s active organization and their role within it are injected into the JWT payload. This allows Go microservices to performing authorization checks without always querying the database.
jwt({
  jwt: {
    definePayload: async ({ user }) => {
      const db = drizzle(this.env.DATABASE_URL);
      const memberRecords = await db.select().from(schema.member).where(eq(schema.member.userId, user.id));
      const activeOrg = memberRecords.length > 0 ? memberRecords[0] : undefined;

      return {
        // ...
        organizationId: activeOrg?.organizationId,
        orgRole: activeOrg?.role, // Maps owner, admin, staff, or viewer
      };
    }
  }
})

Multi-Tenant Isolation

The Go backend enforces isolation by ensuring that the organizationId from the JWT matches the organization_id of the resource being accessed.

Example Check (Go Backend)

func (s *Service) GetReport(ctx context.Context, orgID string) {
    user := middleware.GetUser(ctx)
    if user.OrganizationID != orgID {
        return ForbiddenError
    }
    // ... continue
}

Consistency & Real-time Updates

When a member role is updated or a member is removed via the Bun API, the change is reflected immediately in the members table. To ensure the Go backend’s Permission Cache remains consistent, a NATS message is published to invalidate the local cache for that user.