Appearance
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 organizationPATCH /auth/organization/update- Update organization detailsDELETE /auth/organization/delete- Delete organization (owner only)GET /auth/organization/list- List user's organizationsPOST /auth/organization/set-active- Switch active organization
Member Management
POST /auth/organization/invite- Send invitationPOST /auth/organization/accept-invitation- Accept invitationPOST /auth/organization/remove-member- Remove memberPATCH /auth/organization/update-member-role- Change member roleGET /auth/organization/members- List organization members
Team Management
POST /auth/organization/create-team- Create teamPATCH /auth/organization/update-team- Update teamDELETE /auth/organization/delete-team- Delete teamPOST /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
- Always check organization context in protected routes
- Scope all queries to active organization
- Validate permissions before sensitive operations
- Use Better Auth's built-in methods rather than custom implementation
- 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