Skip to content

Add Teams

Teams let you create subgroups within organizations. This recipe enables Better Auth's teams feature and wires it into the existing schema.

1. Add the schema

Create db/schema/team.ts:

typescript
import { relations } from "drizzle-orm";
import { index, pgTable, text, timestamp, unique } from "drizzle-orm/pg-core";
import { generateId } from "./id";
import { organization } from "./organization";
import { user } from "./user";

export const team = pgTable(
  "team",
  {
    id: text()
      .primaryKey()
      .$defaultFn(() => generateId("tea")),
    name: text().notNull(),
    organizationId: text()
      .notNull()
      .references(() => organization.id, { onDelete: "cascade" }),
    createdAt: timestamp({ withTimezone: true, mode: "date" })
      .defaultNow()
      .notNull(),
    updatedAt: timestamp({ withTimezone: true, mode: "date" })
      .defaultNow()
      .$onUpdate(() => new Date())
      .notNull(),
  },
  (table) => [index("team_organization_id_idx").on(table.organizationId)],
);

export const teamMember = pgTable(
  "team_member",
  {
    id: text()
      .primaryKey()
      .$defaultFn(() => generateId("tmb")),
    teamId: text()
      .notNull()
      .references(() => team.id, { onDelete: "cascade" }),
    userId: text()
      .notNull()
      .references(() => user.id, { onDelete: "cascade" }),
    createdAt: timestamp({ withTimezone: true, mode: "date" })
      .defaultNow()
      .notNull(),
    updatedAt: timestamp({ withTimezone: true, mode: "date" })
      .defaultNow()
      .$onUpdate(() => new Date())
      .notNull(),
  },
  (table) => [
    unique("team_member_team_user_unique").on(table.teamId, table.userId),
    index("team_member_team_id_idx").on(table.teamId),
    index("team_member_user_id_idx").on(table.userId),
  ],
);

export const teamRelations = relations(team, ({ one, many }) => ({
  organization: one(organization, {
    fields: [team.organizationId],
    references: [organization.id],
  }),
  members: many(teamMember),
}));

export const teamMemberRelations = relations(teamMember, ({ one }) => ({
  team: one(team, {
    fields: [teamMember.teamId],
    references: [team.id],
  }),
  user: one(user, {
    fields: [teamMember.userId],
    references: [user.id],
  }),
}));

Export it from db/schema/index.ts:

typescript
export * from "./team"; 

2. Extend session and invitation tables

Add activeTeamId to the session table in db/schema/user.ts:

typescript
export const session = pgTable(
  "session",
  {
    // ...existing columns
    activeOrganizationId: text(),
    activeTeamId: text(), 
  },
  // ...
);

Add teamId to the invitation table in db/schema/invitation.ts for team-scoped invitations:

typescript
export const invitation = pgTable(
  "invitation",
  {
    // ...existing columns
    teamId: text().references(() => team.id, { onDelete: "cascade" }), 
  },
  // ...
);

3. Enable the teams plugin

In apps/api/lib/auth.ts, add the new tables to the Drizzle adapter schema and enable teams in the organization plugin:

typescript
database: drizzleAdapter(db, {
  provider: "pg",
  schema: {
    // ...existing mappings
    team: Db.team, 
    teamMember: Db.teamMember, 
  },
}),

// ...

plugins: [
  organization({
    allowUserToCreateOrganization: true,
    organizationLimit: 5,
    creatorRole: "owner",
    teams: { enabled: true }, 
  }),
],

In apps/app/lib/auth.ts, enable teams on the client:

typescript
export const auth = createAuthClient({
  // ...
  plugins: [
    organizationClient({
      teams: { enabled: true }, 
    }),
    // ...other plugins
  ],
});

4. Apply the migration

bash
bun db:generate
bun db:push

5. Use the teams API

Create a team within the active organization:

ts
await auth.organization.createTeam({
  name: "Engineering",
});

Set the active team for the current session:

ts
await auth.organization.setActiveTeam({
  teamId: "tea_...",
});

List teams and manage members:

ts
// List teams in the active organization
const { data: teams } = await auth.organization.listTeams();

// Add a member to a team
await auth.organization.addTeamMember({
  teamId: "tea_...",
  userId: "usr_...",
});

// Remove a member from a team
await auth.organization.removeTeamMember({
  teamId: "tea_...",
  userId: "usr_...",
});

The active team ID is available in the session as session.activeTeamId, alongside the existing session.activeOrganizationId.

Reference