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:
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:
export * from "./team"; 2. Extend session and invitation tables
Add activeTeamId to the session table in db/schema/user.ts:
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:
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:
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:
export const auth = createAuthClient({
// ...
plugins: [
organizationClient({
teams: { enabled: true },
}),
// ...other plugins
],
});4. Apply the migration
bun db:generate
bun db:push5. Use the teams API
Create a team within the active organization:
await auth.organization.createTeam({
name: "Engineering",
});Set the active team for the current session:
await auth.organization.setActiveTeam({
teamId: "tea_...",
});List teams and manage members:
// 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
- Better Auth organization plugin – Teams
- Organizations & Roles – base organization setup