Skip to content

Organizations Backend Implementation

Backend implementation for multi-tenant organization functionality using Better Auth.

Architecture

Organizations are managed by Better Auth's organization plugin, integrated with AdonisJS and PostgreSQL.

Client Request → Better Auth → PostgreSQL → Response

                   Redis (Session Cache)

Data Models

Organization Model

typescript
class Organization {
  id: string // UUID primary key
  ownerId: string // User who created the organization
  name: string // Organization display name
  slug: string // URL-safe identifier
  logo: string | null // Optional logo URL
  metadata: object | null // Additional custom data
  createdAt: DateTime
  updatedAt: DateTime
}

Member Model

typescript
class Member {
  id: string // Membership record ID
  userId: string // Reference to User
  organizationId: string // Reference to Organization
  role: string // Role: owner, admin, member
  teamId: string | null // Optional team assignment
  createdAt: DateTime
  updatedAt: DateTime
}

Team Model

typescript
class Team {
  id: string // Team ID
  organizationId: string // Parent organization
  name: string // Team name
  metadata: object | null // Custom team data
  createdAt: DateTime
  updatedAt: DateTime
}

Invitation Model

typescript
class Invitation {
  id: string // Invitation ID
  organizationId: string // Target organization
  email: string // Invitee email
  role: string // Assigned role
  invitedBy: string // User who sent invitation
  status: string // pending, accepted, declined
  expiresAt: DateTime // Invitation expiry
  createdAt: DateTime
  updatedAt: DateTime
}

Better Auth Configuration

Organizations are configured in apps/server/app/lib/auth.ts:

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

export const auth = betterAuth({
  // ... other config
  plugins: [
    organization({
      // Organization plugin configuration
      allowUserToCreateOrganization: true,
      organizationRole: ['owner', 'admin', 'member'],
      creatorRole: 'owner',
      invitationExpiryTime: 60 * 60 * 24 * 7, // 7 days
    }),
  ],
})

Session Context

Active Organization

Sessions track the active organization for workspace context:

typescript
class Session {
  // ... other fields
  activeOrganizationId: string | null // Current workspace
}

This allows:

  • Automatic organization scoping for queries
  • Quick workspace switching without re-authentication
  • Organization-specific permissions checking

API Endpoints

All organization endpoints are handled by Better Auth through the /auth/organization/* routes:

Organization Management

  • POST /auth/organization/create - Create new organization
  • PATCH /auth/organization/update - Update organization details
  • DELETE /auth/organization/delete - Delete organization (owner only)
  • GET /auth/organization/list - List user's organizations
  • POST /auth/organization/set-active - Switch active organization

Member Management

  • POST /auth/organization/invite - Send invitation
  • POST /auth/organization/accept-invitation - Accept invitation
  • POST /auth/organization/remove-member - Remove member
  • PATCH /auth/organization/update-member-role - Change member role
  • GET /auth/organization/members - List organization members

Team Management

  • POST /auth/organization/create-team - Create team
  • PATCH /auth/organization/update-team - Update team
  • DELETE /auth/organization/delete-team - Delete team
  • POST /auth/organization/add-team-member - Add member to team

Database Schema

Migration Example

typescript
// Create organizations table
export default class extends BaseSchema {
  protected tableName = 'organizations'

  async up() {
    this.schema.createTable(this.tableName, (table) => {
      table.uuid('id').primary()
      table.uuid('owner_id').references('id').inTable('users')
      table.string('name').notNullable()
      table.string('slug').notNullable().unique()
      table.string('logo').nullable()
      table.jsonb('metadata').nullable()
      table.timestamp('created_at').notNullable()
      table.timestamp('updated_at').nullable()

      table.index(['owner_id'])
      table.index(['slug'])
    })
  }
}

Security Implementation

Permission Checking

Better Auth handles permission validation automatically:

typescript
// Middleware checks organization membership and role
auth.api.organizationMiddleware({
  requiredRole: 'admin', // Minimum role required
  checkOrganizationId: true, // Verify organization context
})

Data Isolation

All queries should be scoped to the active organization:

typescript
// Example: Get organization events
const events = await Event.query()
  .where('organization_id', session.activeOrganizationId)
  .orderBy('created_at', 'desc')

Environment Configuration

No specific environment variables required - organizations use the existing Better Auth and database configuration.

Integration with Features

Events (Future)

typescript
class Event {
  // ... other fields
  organizationId: string // Organization ownership
}

Resources (Future)

All resources will include organizationId for multi-tenant isolation.

Best Practices

  1. Always check organization context in protected routes
  2. Scope all queries to active organization
  3. Validate permissions before sensitive operations
  4. Use Better Auth's built-in methods rather than custom implementation
  5. Cache organization list on frontend for quick switching

Troubleshooting

Common Issues

Organization not found:

  • Verify organization ID exists
  • Check user membership
  • Ensure session has correct active organization

Permission denied:

  • Verify user role in organization
  • Check if operation requires higher role
  • Ensure Better Auth middleware is applied

Session not updating:

  • Clear Redis cache after organization switch
  • Verify session refresh after role changes
  • Check if frontend is refetching session

Built with ❤️ by the Jubiloop team