toeverything/AFFiNE · error · Error

Cannot remove the last active workspace owner.

Error message

Cannot remove the last active workspace owner.

What it means

Thrown by WorkspaceMemberModel.delete (permission-write.ts:254) when removing a user would leave the workspace with zero active owners. The method first takes a pg_advisory_xact lock and a SELECT ... FOR UPDATE on owner rows, then counts other active owners; if the target is an owner and no other active owner exists, it refuses the delete so a workspace is never orphaned. Note this is a plain Error (not a UserFriendlyError), so unless a higher layer maps it, it surfaces as an internal_server_error.

Source

Thrown at packages/backend/server/src/models/permission-write.ts:254

    const existingOwners = await this.db.workspaceMember.count({
      where: {
        workspaceId,
        role: 'owner',
        state: 'active',
        userId: { not: userId },
      },
    });
    const deletingOwner = await this.db.workspaceMember.count({
      where: {
        workspaceId,
        userId,
        role: 'owner',
        state: 'active',
      },
    });

    if (deletingOwner > 0 && existingOwners === 0) {
      throw new Error('Cannot remove the last active workspace owner.');
    }

    return await this.db.workspaceMember.deleteMany({
      where: { workspaceId, userId, state: 'active' },
    });
  }
}

@Injectable()
export class WorkspaceInvitationModel extends BaseModel {
  @Transactional()
  async set(
    workspaceId: string,
    userId: string,
    role: WorkspaceRole,
    status: WorkspaceMemberStatus,
    data: {
      source?: WorkspaceMemberSource;

View on GitHub (pinned to 26c515e050)

Solutions

  1. Promote another active member to owner first via WorkspaceUserModel.setOwner(workspaceId, otherMemberId) before calling delete.
  2. If the intent is to leave, transfer ownership to a different active member, then retry the removal.
  3. If the workspace should be destroyed, delete the workspace itself rather than removing its owner.

Example fix

// before
await models.workspaceMember.delete(wsId, ownerUserId);
// after
await models.workspaceUser.setOwner(wsId, otherMemberId);
await models.workspaceMember.delete(wsId, ownerUserId);
Defensive patterns

Strategy: validation

Validate before calling

async function safeRemoveOwner(models, wsId: string, userId: string) {
  const otherOwners = await models.db.workspaceMember.count({
    where: { workspaceId: wsId, role: 'owner', state: 'active', userId: { not: userId } },
  });
  if (otherOwners === 0) {
    throw new Error('Refusing to remove the last active workspace owner; transfer ownership first.');
  }
  return models.workspaceMember.delete(wsId, userId);
}

Prevention

When it happens

Trigger: Calling the workspace member removal / 'leave workspace' / revoke-permission path for the only active owner of a workspace. Reproduce: create a workspace (you become the sole owner), invite no second owner, then call WorkspaceMemberModel.delete(workspaceId, ownerUserId).

Common situations: The sole workspace owner clicks 'Leave workspace'; an automated offboarding script removes members and accidentally targets the owner; a prior ownership transfer failed silently so the delete now strands the workspace.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/9e9e56f1acbf44c2. Report an issue: GitHub.