Skip to main content

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

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):
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

id
text
required
Unique identifier for the organization
name
text
required
Display name of the organizationExample: "Acme Corporation"
slug
text
required
Unique URL-friendly identifierExample: "acme-corp"
URL to organization logo image
createdAt
timestamp
required
When the organization was created
metadata
text
JSON string for additional custom data
The slug field has a unique index ensuring no two organizations share the same URL path.

Member Management

From auth-schema.ts (lines 107-124):
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

id
text
required
Unique member relationship ID
organizationId
text
required
Reference to the organizationOn Delete: CASCADE - members are deleted when organization is deleted
userId
text
required
Reference to the userOn Delete: CASCADE - membership is removed when user is deleted
role
text
default:"member"
Member’s role within the organizationCommon roles: owner, admin, member
createdAt
timestamp
required
When the user joined the organization

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):
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

id
text
required
Unique invitation identifier
organizationId
text
required
Organization the invitation is for
email
text
required
Email address of the invitee
role
text
Role the invitee will have upon accepting
status
text
default:"pending"
Invitation status: pending, accepted, rejected, expired
expiresAt
timestamp
required
Expiration date/time for the invitation
createdAt
timestamp
required
When the invitation was created
inviterId
text
required
User who sent the invitation (must be org admin/owner)
Invitations automatically expire after the expiresAt timestamp. Check status before allowing acceptance.

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:
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)],
);
activeOrganizationId
text
ID of the organization currently active in this sessionAllows users to switch between organizations they belong to
Users can belong to multiple organizations. The activeOrganizationId determines which org context is used for creating resources.

Relationships

From auth-schema.ts (lines 176-201), Drizzle relations define the connections:
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

Organization Roles

Better Auth organization plugin supports hierarchical roles:

Default Roles

owner
role
Full control over organization, including deletion
  • Manage all members and invitations
  • Delete organization
  • Change organization settings
  • Create and manage all resources
admin
role
Administrative privileges
  • Invite and manage members
  • Manage organization settings
  • Create and manage resources
  • Cannot delete organization
member
role
Basic member access
  • View organization resources
  • Create resources under organization
  • Cannot invite users
  • Cannot change settings
Custom roles can be defined in Better Auth configuration to match your specific permission requirements.

Organization-Scoped Resources

Blog Posts

From blog_posts.schema.ts:
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:
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:
export const blogAuthors = pgTable("blog_authors", {
  id: text("id").primaryKey(),
  worksForId: text("works_for_id"),
  // ... other fields
});
The worksForId field links authors to organizations, establishing organizational attribution for content.

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

// 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:
// 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 for implementation details.

Build docs developers (and LLMs) love