Skip to main content

Team Roles

Stack Auth uses a permission-based system for team access control. While there are no predefined “roles” in the traditional sense, the platform provides default permission sets for different member types.

Member Types

When users are added to teams, they are assigned one of two types:

1. Creator

The user who creates a team is automatically assigned as the creator. Creators receive the teamCreator default permissions.
// Creating a team automatically makes you the creator
const team = await stackServerApp.createTeam({
  displayName: "My Team",
  creator_user_id: "me" // Current user becomes creator
});
From apps/backend/src/app/api/latest/teams/crud.tsx:88:
if (addUserId) {
  await ensureUserExists(tx, { 
    tenancyId: auth.tenancy.id, 
    userId: addUserId 
  });
  await addUserToTeam(tx, {
    tenancy: auth.tenancy,
    teamId: db.teamId,
    userId: addUserId,
    type: 'creator', // <-- Creator type
  });
}

2. Member

Users who join a team (via invitation or direct addition) are assigned as members. Members receive the teamMember default permissions.
// Adding a user to a team makes them a member
await stackServerApp.addTeamMember({
  team_id: "team-uuid",
  user_id: "user-uuid" // This user becomes a member
});
From apps/backend/src/app/api/latest/team-memberships/crud.tsx:80:
return await addUserToTeam(tx, {
  tenancy: auth.tenancy,
  teamId: params.team_id,
  userId: params.user_id,
  type: 'member', // <-- Member type
});

Default Permissions

Default permissions are automatically granted when users join teams. These are configured in your project’s RBAC settings. From apps/backend/src/lib/permissions.tsx:514:
export async function grantDefaultTeamPermissions(
  tx: PrismaTransaction,
  options: {
    tenancy: Tenancy,
    userId: string,
    teamId: string,
    type: "creator" | "member",
  }
) {
  const config = options.tenancy.config;
  
  const defaultPermissions = config.rbac.defaultPermissions[
    options.type === "creator" ? "teamCreator" : "teamMember"
  ];
  
  for (const permissionId of Object.keys(defaultPermissions)) {
    await grantTeamPermission(tx, {
      tenancy: options.tenancy,
      teamId: options.teamId,
      userId: options.userId,
      permissionId: permissionId,
    });
  }
  
  return {
    grantedPermissionIds: Object.keys(defaultPermissions),
  };
}

Configuration

You can configure default permissions in your project settings:
{
  rbac: {
    defaultPermissions: {
      teamCreator: {
        "$update_team": true,
        "$delete_team": true,
        "$read_members": true,
        "$remove_members": true,
        "$invite_members": true,
        "$manage_api_keys": true
      },
      teamMember: {
        "$read_members": true
      }
    }
  }
}

Custom Roles via Permission Groups

While Stack Auth doesn’t have built-in “roles,” you can implement role-like behavior using permission groups:
// Define a "Manager" role as a permission group
const managerPermission = await stackServerApp.createTeamPermission({
  id: "manager",
  description: "Team manager role",
  contained_permission_ids: [
    "$update_team",
    "$invite_members",
    "$read_members"
  ]
});

// Grant the "Manager" role to a user
await stackServerApp.grantTeamPermission({
  team_id: "team-uuid",
  user_id: "user-uuid",
  permission_id: "manager" // Grants all contained permissions
});

Checking Member Type

You can determine if a user is the creator by checking their permissions:
// List user's permissions on the team
const permissions = await stackServerApp.listTeamPermissions({
  team_id: "team-uuid",
  user_id: "user-uuid",
  recursive: false // Direct permissions only
});

// Creators typically have $delete_team permission
const isCreator = permissions.items.some(
  p => p.id === "$delete_team"
);

Team Member Schema

model TeamMember {
  tenancyId       String @db.Uuid
  projectUserId   String @db.Uuid
  teamId          String @db.Uuid
  displayName     String?        // Optional override
  profileImageUrl String?        // Optional override
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
  isSelected      BooleanTrue?   // Currently selected team
  
  teamMemberDirectPermissions TeamMemberDirectPermission[]
  
  @@id([tenancyId, projectUserId, teamId])
}

Per-Team User Profiles

Team members can have different display names and profile images per team:
// Update user's profile for a specific team
await stackServerApp.updateTeamMemberProfile({
  team_id: "team-uuid",
  user_id: "user-uuid",
  display_name: "Work Name",
  profile_image_url: "https://..."
});
This allows users to have different identities in different teams while maintaining a single user account.

Selected Team

Users can have one “selected” team at a time, useful for UI state:
// The isSelected field ensures only one team is selected
@@unique([tenancyId, projectUserId, isSelected])
This database constraint ensures a user can only have one team marked as selected across all their team memberships.

Best Practices

  1. Use Permission Groups: Instead of managing individual permissions, create permission groups that represent roles
  2. Document Your Roles: Maintain documentation of what each custom “role” (permission group) represents
  3. Audit Permissions: Regularly review which permissions are granted to which member types
  4. Principle of Least Privilege: Only grant the minimum permissions needed for each role

Migration from Role-Based Systems

If you’re migrating from a traditional role-based system:
// Old role-based approach
enum Role {
  ADMIN = "admin",
  MANAGER = "manager",
  MEMBER = "member"
}

// New permission-based approach
const roles = {
  admin: {
    id: "admin",
    contained_permission_ids: [
      "$update_team",
      "$delete_team",
      "$read_members",
      "$remove_members",
      "$invite_members",
      "$manage_api_keys"
    ]
  },
  manager: {
    id: "manager",
    contained_permission_ids: [
      "$read_members",
      "$invite_members"
    ]
  },
  member: {
    id: "member",
    contained_permission_ids: [
      "$read_members"
    ]
  }
};

// Create permission definitions for each role
for (const role of Object.values(roles)) {
  await stackServerApp.createTeamPermissionDefinition(role);
}
See also:

Build docs developers (and LLMs) love