> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/Ishaq74/concordia/llms.txt
> Use this file to discover all available pages before exploring further.

# Organizations & Teams

> Organization management with team membership, role-based access, and invitations powered by Better Auth

# Organizations & Teams

Concordia uses Better Auth's organization plugin to provide multi-tenant team management with role-based access control, member management, and invitation workflows.

## Overview

Organizations are implemented in `src/database/schemas/auth-schema.ts` using Better Auth's built-in organization features. This provides:

* Multi-user organizations with hierarchical roles
* Member invitation system
* Active organization context per session
* Organization-scoped resources (blogs, services, etc.)

## Organization Schema

From `src/database/schemas/auth-schema.ts` (lines 94-105):

```typescript theme={null}
export const organization = pgTable(
  "organization",
  {
    id: text("id").primaryKey(),
    name: text("name").notNull(),
    slug: text("slug").notNull().unique(),
    logo: text("logo"),
    createdAt: timestamp("created_at").notNull(),
    metadata: text("metadata"),
  },
  (table) => [uniqueIndex("organization_slug_uidx").on(table.slug)],
);
```

### Organization Fields

<ParamField path="id" type="text" required>
  Unique identifier for the organization
</ParamField>

<ParamField path="name" type="text" required>
  Display name of the organization

  Example: `"Acme Corporation"`
</ParamField>

<ParamField path="slug" type="text" required>
  Unique URL-friendly identifier

  Example: `"acme-corp"`
</ParamField>

<ParamField path="logo" type="text">
  URL to organization logo image
</ParamField>

<ParamField path="createdAt" type="timestamp" required>
  When the organization was created
</ParamField>

<ParamField path="metadata" type="text">
  JSON string for additional custom data
</ParamField>

<Info>
  The `slug` field has a unique index ensuring no two organizations share the same URL path.
</Info>

## Member Management

From `auth-schema.ts` (lines 107-124):

```typescript theme={null}
export const member = pgTable(
  "member",
  {
    id: text("id").primaryKey(),
    organizationId: text("organization_id")
      .notNull()
      .references(() => organization.id, { onDelete: "cascade" }),
    userId: text("user_id")
      .notNull()
      .references(() => user.id, { onDelete: "cascade" }),
    role: text("role").default("member").notNull(),
    createdAt: timestamp("created_at").notNull(),
  },
  (table) => [
    index("member_organizationId_idx").on(table.organizationId),
    index("member_userId_idx").on(table.userId),
  ],
);
```

### Member Fields

<ParamField path="id" type="text" required>
  Unique member relationship ID
</ParamField>

<ParamField path="organizationId" type="text" required>
  Reference to the organization

  **On Delete**: CASCADE - members are deleted when organization is deleted
</ParamField>

<ParamField path="userId" type="text" required>
  Reference to the user

  **On Delete**: CASCADE - membership is removed when user is deleted
</ParamField>

<ParamField path="role" type="text" default="member">
  Member's role within the organization

  Common roles: `owner`, `admin`, `member`
</ParamField>

<ParamField path="createdAt" type="timestamp" required>
  When the user joined the organization
</ParamField>

### Member Indexes

Two indexes optimize member queries:

1. **By Organization**: `member_organizationId_idx` - Fast lookup of all members in an org
2. **By User**: `member_userId_idx` - Fast lookup of all orgs a user belongs to

## Invitation System

From `auth-schema.ts` (lines 126-146):

```typescript theme={null}
export const invitation = pgTable(
  "invitation",
  {
    id: text("id").primaryKey(),
    organizationId: text("organization_id")
      .notNull()
      .references(() => organization.id, { onDelete: "cascade" }),
    email: text("email").notNull(),
    role: text("role"),
    status: text("status").default("pending").notNull(),
    expiresAt: timestamp("expires_at").notNull(),
    createdAt: timestamp("created_at").defaultNow().notNull(),
    inviterId: text("inviter_id")
      .notNull()
      .references(() => user.id, { onDelete: "cascade" }),
  },
  (table) => [
    index("invitation_organizationId_idx").on(table.organizationId),
    index("invitation_email_idx").on(table.email),
  ],
);
```

### Invitation Fields

<ParamField path="id" type="text" required>
  Unique invitation identifier
</ParamField>

<ParamField path="organizationId" type="text" required>
  Organization the invitation is for
</ParamField>

<ParamField path="email" type="text" required>
  Email address of the invitee
</ParamField>

<ParamField path="role" type="text">
  Role the invitee will have upon accepting
</ParamField>

<ParamField path="status" type="text" default="pending">
  Invitation status: `pending`, `accepted`, `rejected`, `expired`
</ParamField>

<ParamField path="expiresAt" type="timestamp" required>
  Expiration date/time for the invitation
</ParamField>

<ParamField path="createdAt" type="timestamp" required>
  When the invitation was created
</ParamField>

<ParamField path="inviterId" type="text" required>
  User who sent the invitation (must be org admin/owner)
</ParamField>

<Warning>
  Invitations automatically expire after the `expiresAt` timestamp. Check status before allowing acceptance.
</Warning>

### Invitation Indexes

1. **By Organization**: `invitation_organizationId_idx` - List all invitations for an org
2. **By Email**: `invitation_email_idx` - Find pending invitations for a user

## Session Context

From `auth-schema.ts` (lines 32-51), sessions track the active organization:

```typescript theme={null}
export const session = pgTable(
  "session",
  {
    id: text("id").primaryKey(),
    expiresAt: timestamp("expires_at").notNull(),
    token: text("token").notNull().unique(),
    createdAt: timestamp("created_at").defaultNow().notNull(),
    updatedAt: timestamp("updated_at")
      .$onUpdate(() => new Date())
      .notNull(),
    ipAddress: text("ip_address"),
    userAgent: text("user_agent"),
    userId: text("user_id")
      .notNull()
      .references(() => user.id, { onDelete: "cascade" }),
    activeOrganizationId: text("active_organization_id"),
    impersonatedBy: text("impersonated_by"),
  },
  (table) => [index("session_userId_idx").on(table.userId)],
);
```

<ParamField path="activeOrganizationId" type="text">
  ID of the organization currently active in this session

  Allows users to switch between organizations they belong to
</ParamField>

<Info>
  Users can belong to multiple organizations. The `activeOrganizationId` determines which org context is used for creating resources.
</Info>

## Relationships

From `auth-schema.ts` (lines 176-201), Drizzle relations define the connections:

```typescript theme={null}
export const organizationRelations = relations(organization, ({ many }) => ({
  members: many(member),
  invitations: many(invitation),
}));

export const memberRelations = relations(member, ({ one }) => ({
  organization: one(organization, {
    fields: [member.organizationId],
    references: [organization.id],
  }),
  user: one(user, {
    fields: [member.userId],
    references: [user.id],
  }),
}));

export const invitationRelations = relations(invitation, ({ one }) => ({
  organization: one(organization, {
    fields: [invitation.organizationId],
    references: [organization.id],
  }),
  user: one(user, {
    fields: [invitation.inviterId],
    references: [user.id],
  }),
}));
```

### Relationship Diagram

```mermaid theme={null}
erDiagram
    USER ||--o{ MEMBER : has
    USER ||--o{ INVITATION : sends
    ORGANIZATION ||--o{ MEMBER : contains
    ORGANIZATION ||--o{ INVITATION : has
    SESSION }o--|| USER : belongs_to
    SESSION }o--o| ORGANIZATION : active_in
```

## Organization Roles

Better Auth organization plugin supports hierarchical roles:

### Default Roles

<ParamField path="owner" type="role">
  Full control over organization, including deletion

  * Manage all members and invitations
  * Delete organization
  * Change organization settings
  * Create and manage all resources
</ParamField>

<ParamField path="admin" type="role">
  Administrative privileges

  * Invite and manage members
  * Manage organization settings
  * Create and manage resources
  * Cannot delete organization
</ParamField>

<ParamField path="member" type="role">
  Basic member access

  * View organization resources
  * Create resources under organization
  * Cannot invite users
  * Cannot change settings
</ParamField>

<Note>
  Custom roles can be defined in Better Auth configuration to match your specific permission requirements.
</Note>

## Organization-Scoped Resources

### Blog Posts

From `blog_posts.schema.ts`:

```typescript theme={null}
export const blogPosts = pgTable("blog_posts", {
  id: text("id").primaryKey(),
  slug: text("slug").notNull().unique(),
  organizationId: text("organization_id"),
  // ... other fields
});
```

Blog posts can belong to an organization, enabling organization blogs.

### Service Listings

From `services_listings.schema.ts`:

```typescript theme={null}
export const servicesListings = pgTable("services_listings", {
  id: text("id").primaryKey(),
  slug: text("slug").notNull().unique(),
  providerId: text("provider_id").notNull(),
  organizationId: text("organization_id"),
  // ... other fields
});
```

Services can be offered by organizations or individual providers.

### Authors

From `blog_authors.schema.ts`:

```typescript theme={null}
export const blogAuthors = pgTable("blog_authors", {
  id: text("id").primaryKey(),
  worksForId: text("works_for_id"),
  // ... other fields
});
```

<Info>
  The `worksForId` field links authors to organizations, establishing organizational attribution for content.
</Info>

## Invitation Workflow

1. **Create Invitation**: Admin/owner creates invitation with email and role
2. **Send Email**: System sends invitation link to email address
3. **User Accepts**: User clicks link and accepts invitation
4. **Create Membership**: System creates `member` record with specified role
5. **Update Invitation**: Invitation status changes to `accepted`

### Example Invitation Flow

```typescript theme={null}
// 1. Create invitation (admin action)
const invitation = {
  id: generateId(),
  organizationId: "org-123",
  email: "newmember@example.com",
  role: "member",
  status: "pending",
  expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
  inviterId: "admin-user-id",
};

// 2. User accepts (creates member record)
const member = {
  id: generateId(),
  organizationId: invitation.organizationId,
  userId: acceptingUserId,
  role: invitation.role,
  createdAt: new Date(),
};

// 3. Update invitation status
invitation.status = "accepted";
```

## Multi-Tenancy Pattern

Organizations enable multi-tenancy where:

1. **Users** can belong to multiple organizations
2. **Resources** can be scoped to an organization
3. **Sessions** track active organization context
4. **Permissions** are enforced per organization

### Switching Organizations

Users switch active organization by updating their session:

```typescript theme={null}
// Update session's activeOrganizationId
session.activeOrganizationId = "org-456";

// All subsequent resource creation uses this org context
```

## Best Practices

### Member Management

1. Always verify user's role before allowing privileged actions
2. Use cascade deletes to maintain referential integrity
3. Index lookups by both `organizationId` and `userId` for performance

### Invitation Security

1. Set reasonable expiration times (7-14 days)
2. Verify invitation status before acceptance
3. Check that email matches authenticated user
4. Clean up expired invitations periodically

### Organization Isolation

1. Always filter queries by organization ID
2. Use session's `activeOrganizationId` for context
3. Validate user has access to organization before showing resources
4. Consider using RLS (Row Level Security) policies

## Integration with Better Auth

The organization plugin is configured in Better Auth setup and provides:

* Automatic organization context in auth session
* Built-in member and invitation management APIs
* Role-based access control helpers
* Organization switching functionality

Refer to [Better Auth Organization Plugin](https://www.better-auth.com/docs/plugins/organization) for implementation details.
